diff --git a/Arkham SCE.json b/Arkham SCE.json index 78882ac..a83ed21 100644 --- a/Arkham SCE.json +++ b/Arkham SCE.json @@ -116,10 +116,6 @@ "displayed": "chaosBag", "normalized": "chaosbag" }, - { - "displayed": "ActionToken", - "normalized": "actiontoken" - }, { "displayed": "LargeBox", "normalized": "largebox" @@ -131,6 +127,10 @@ { "displayed": "CameraZoom_ignore", "normalized": "camerazoom_ignore" + }, + { + "displayed": "UniversalToken", + "normalized": "universaltoken" } ] }, @@ -166,12 +166,12 @@ "URL": "http://cloud-3.steamusercontent.com/ugc/2026086584372569912/5CB461AEAE2E59D3064D90A776EB86C46081EC78/" }, { - "Name": "option-on", + "Name": "option_on", "Type": 0, "URL": "http://cloud-3.steamusercontent.com/ugc/2462982115668997008/2178787B67B3C96F3419EDBAB8420E39893756BC/" }, { - "Name": "option-off", + "Name": "option_off", "Type": 0, "URL": "http://cloud-3.steamusercontent.com/ugc/2462982115668996901/D6438ECBB11DECC6DB9987589FF526FBAD4D2368/" }, @@ -205,16 +205,6 @@ "Type": 0, "URL": "http://cloud-3.steamusercontent.com/ugc/2280574378889753733/F67B7B37FF7AA253B6D697E577DF54A3E76030C2/" }, - { - "Name": "option_on", - "Type": 0, - "URL": "http://cloud-3.steamusercontent.com/ugc/2024962321889555728/22ABD35CBB49A001F3A5318E4AFCFB22D24FEA39/" - }, - { - "Name": "option_off", - "Type": 0, - "URL": "http://cloud-3.steamusercontent.com/ugc/2024962321889555661/6643E5CC9160FF4624672C255D0DF7B313DA00A5/" - }, { "Name": "SpeechBubble", "Type": 0, @@ -380,13 +370,18 @@ "Type": 0, "URL": "http://cloud-3.steamusercontent.com/ugc/2510267932886739653/CB7AA2D73777EF5938A6E6CD664B2ABA52B6E20A/" }, + { + "Name": "token-eldersign", + "Type": 0, + "URL": "http://cloud-3.steamusercontent.com/ugc/2540675016035917168/C0D6F531A85FD94C2F54825DFC50781B5B499A1D/" + }, { "Name": "token-custom-token", "Type": 0, "URL": "http://cloud-3.steamusercontent.com/ugc/2380784374792650571/E4C2B2B69282A4EE15FE290FF6B08BEFC8FCA65C/" } ], - "Date": "Sun May 12 13:10:46 CEST 2024", + "Date": "Mon Jul 8 20:19:48 CEST 2024", "DecalPallet": [ { "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1474319121424323663/BC5570ECF747F1B30224461B576E8B0FE7FA5F33/", @@ -400,7 +395,7 @@ } ], "Decals": [], - "EpochTime": 1715512246, + "EpochTime": 1720462788, "GameComplexity": "", "GameMode": "Arkham Horror LCG - Super Complete Edition", "GameType": "", @@ -459,8 +454,8 @@ "LutIndex": 0, "ReflectionIntensity": 1 }, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/Global\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal soundCubeApi = require(\"core/SoundCubeApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n---------------------------------------------------------\n-- general setup\n---------------------------------------------------------\n\nENCOUNTER_DECK_POS = { -3.93, 1, 5.76 }\nENCOUNTER_DECK_DISCARD_POSITION = { -3.85, 1, 10.38 }\n\n-- GUIDs that will not be interactable (e.g. parts of the table)\nlocal NOT_INTERACTABLE = {\n \"6161b4\", -- Decoration-Map\n \"9f334f\", -- MythosArea\n \"463022\", -- Panel behind tentacle stand\n \"f182ee\", -- InvestigatorCount\n \"7bff34\", -- Tentacle stand\n \"8646eb\", -- horizontal border left\n \"75937e\", -- horizontal border right\n \"612072\", -- vertical border left\n \"975c39\", -- vertical border right\n}\n\nlocal chaosTokens = {}\nlocal chaosTokensLastMatGUID = nil\n\n-- chaos token stat tracking\nlocal tokenDrawingStats = { [\"Overall\"] = {} }\n\nlocal bagSearchers = {}\nlocal hideTitleSplashWaitFunctionId = nil\n\n-- online functionality related variables\nlocal MOD_VERSION = \"3.8.0\"\nlocal SOURCE_REPO = 'https://raw.githubusercontent.com/chr1z93/loadable-objects/main'\nlocal library, requestObj, modMeta\nlocal acknowledgedUpgradeVersions = {}\nlocal contentToShow = \"campaigns\"\nlocal currentListItem = 1\nlocal tabIdTable = {\n tab1 = \"campaigns\",\n tab2 = \"scenarios\",\n tab3 = \"fanmadeCampaigns\",\n tab4 = \"fanmadeScenarios\",\n tab5 = \"fanmadePlayerCards\"\n}\n\n-- optionPanel data (intentionally not local!)\noptionPanel = {}\nlocal LANGUAGES = {\n { code = \"zh_CN\", name = \"简体中文\" },\n { code = \"zh_TW\", name = \"繁體中文\" },\n { code = \"de\", name = \"Deutsch\" },\n { code = \"en\", name = \"English\" },\n { code = \"es\", name = \"Español\" },\n { code = \"fr\", name = \"Français\" },\n { code = \"it\", name = \"Italiano\" }\n}\nlocal RESOURCE_OPTIONS = {\n \"enabled\",\n \"custom\",\n \"disabled\"\n}\n\n---------------------------------------------------------\n-- data for tokens\n---------------------------------------------------------\n\nTOKEN_DATA = {\n damage = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357115146/903D11AAE7BD5C254C8DC136E9202EE516289DEA/\", scale = { 0.17, 0.17, 0.17 } },\n horror = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357163535/6D9E0756503664D65BDB384656AC6D4BD713F5FC/\", scale = { 0.17, 0.17, 0.17 } },\n resource = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/\", scale = { 0.17, 0.17, 0.17 } },\n doom = { image = \"https://i.imgur.com/EoL7yaZ.png\", scale = { 0.17, 0.17, 0.17 } },\n clue = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357164917/1D06F1DC4D6888B6F57124BD2AFE20D0B0DA15A8/\", scale = { 0.15, 0.15, 0.15 } }\n}\n\nID_URL_MAP = {\n ['blue'] = { name = \"Elder Sign\", url = 'https://i.imgur.com/nEmqjmj.png' },\n ['p1'] = { name = \"+1\", url = 'https://i.imgur.com/uIx8jbY.png' },\n ['0'] = { name = \"0\", url = 'https://i.imgur.com/btEtVfd.png' },\n ['m1'] = { name = \"-1\", url = 'https://i.imgur.com/w3XbrCC.png' },\n ['m2'] = { name = \"-2\", url = 'https://i.imgur.com/bfTg2hb.png' },\n ['m3'] = { name = \"-3\", url = 'https://i.imgur.com/yfs8gHq.png' },\n ['m4'] = { name = \"-4\", url = 'https://i.imgur.com/qrgGQRD.png' },\n ['m5'] = { name = \"-5\", url = 'https://i.imgur.com/3Ym1IeG.png' },\n ['m6'] = { name = \"-6\", url = 'https://i.imgur.com/c9qdSzS.png' },\n ['m7'] = { name = \"-7\", url = 'https://i.imgur.com/4WRD42n.png' },\n ['m8'] = { name = \"-8\", url = 'https://i.imgur.com/9t3rPTQ.png' },\n ['skull'] = { name = \"Skull\", url = 'https://i.imgur.com/stbBxtx.png' },\n ['cultist'] = { name = \"Cultist\", url = 'https://i.imgur.com/VzhJJaH.png' },\n ['tablet'] = { name = \"Tablet\", url = 'https://i.imgur.com/1plY463.png' },\n ['elder'] = { name = \"Elder Thing\", url = 'https://i.imgur.com/ttnspKt.png' },\n ['red'] = { name = \"Auto-fail\", url = 'https://i.imgur.com/lns4fhz.png' },\n ['bless'] = { name = \"Bless\", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/' },\n ['curse'] = { name = \"Curse\", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/' },\n ['frost'] = { name = \"Frost\", url = 'http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/' }\n}\n\n---------------------------------------------------------\n-- general code\n---------------------------------------------------------\n\n-- saving state of optionPanel to restore later\nfunction onSave()\n local chaosTokensGUID = {}\n for _, obj in ipairs(chaosTokens) do\n if obj ~= nil then\n table.insert(chaosTokensGUID, obj.getGUID())\n end\n end\n\n return JSON.encode({\n optionPanel = optionPanel,\n acknowledgedUpgradeVersions = acknowledgedUpgradeVersions,\n chaosTokensLastMatGUID = chaosTokensLastMatGUID,\n chaosTokensGUID = chaosTokensGUID\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n optionPanel = loadedData.optionPanel\n acknowledgedUpgradeVersions = loadedData.acknowledgedUpgradeVersions\n chaosTokensLastMatGUID = loadedData.chaosTokensLastMatGUID\n\n -- restore saved state for drawn chaos tokens\n for _, guid in ipairs(loadedData.chaosTokensGUID or {}) do\n table.insert(chaosTokens, getObjectFromGUID(guid))\n end\n\n updateOptionPanelState()\n end\n\n for _, guid in ipairs(NOT_INTERACTABLE) do\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then obj.interactable = false end\n end\n\n getModVersion()\n math.randomseed(os.time())\n\n -- initialization of loadable objects library (delay to let Navigation Overlay build)\n Wait.time(function()\n WebRequest.get(SOURCE_REPO .. '/library.json', libraryDownloadCallback)\n end, 1)\nend\n\n-- Event hook for any object search. When chaos tokens are manipulated while the chaos bag\n-- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the\n-- chaos bag during search operations to avoid this.\nfunction onObjectSearchStart(object, playerColor)\n local chaosBag = findChaosBag()\n if object == chaosBag then\n bagSearchers[playerColor] = true\n end\nend\n\n-- Event hook for any object search. When chaos tokens are manipulated while the chaos bag\n-- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the\n-- chaos bag during search operations to avoid this.\nfunction onObjectSearchEnd(object, playerColor)\n local chaosBag = findChaosBag()\n if object == chaosBag then\n bagSearchers[playerColor] = nil\n end\nend\n\n-- Pass object enter container events to the PlayArea to clear vector lines from dragged cards.\n-- This requires the try method as cards won't exist any more after they enter a deck, so the lines\n-- can't be cleared.\nfunction tryObjectEnterContainer(container, object)\n playAreaApi.tryObjectEnterContainer(container, object)\n return true\nend\n\n-- TTS event for objects that enter zones\n-- used to detect the \"token discard zones\" beneath the hand zones\nfunction onObjectEnterZone(zone, enteringObj)\n if zone.getName() ~= \"TokenDiscardZone\" then return end\n if tokenChecker.isChaosToken(enteringObj) then return end\n\n if enteringObj.type == \"Tile\" and enteringObj.getMemo() and enteringObj.getLock() == false then\n local matcolor = playmatApi.getMatColorByPosition(enteringObj.getPosition())\n local trash = guidReferenceApi.getObjectByOwnerAndType(matcolor, \"Trash\")\n trash.putObject(enteringObj)\n end\nend\n\n-- handle card drawing via number typing for multihanded gameplay\n-- (and additionally allow Norman Withers to draw multiple cards via number)\nfunction onObjectNumberTyped(hoveredObject, playerColor, number)\n -- only continue for decks or cards\n if hoveredObject.type ~= \"Deck\" and hoveredObject.type ~= \"Card\" then return end\n\n -- check whether the hovered object is part of a players draw objects\n for _, color in ipairs(playmatApi.getUsedMatColors()) do\n local deckAreaObjects = playmatApi.getDeckAreaObjects(color)\n if deckAreaObjects.topCard == hoveredObject or deckAreaObjects.draw == hoveredObject then\n playmatApi.drawCardsWithReshuffle(color, number)\n return true\n end\n end\n\n -- check if this is a card with states (and then change state instead of drawing it)\n local states = hoveredObject.getStates()\n if states ~= nil and #states \u003e 0 then\n local stateId = hoveredObject.getStateId()\n if stateId ~= number and (#states + 1) \u003e= number then\n hoveredObject.setState(number)\n return true\n end\n end\nend\n\n---------------------------------------------------------\n-- chaos token drawing\n---------------------------------------------------------\n\n-- checks scripting zone for chaos bag (also called by a lot of objects!)\nfunction findChaosBag()\n local chaosBagZone = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"ChaosBagZone\")\n\n -- error handling: scripting zone not found\n if chaosBagZone == nil then\n printToAll(\"Zone for chaos bag detection couldn't be found.\", \"Red\")\n return\n end\n\n for _, item in ipairs(chaosBagZone.getObjects()) do\n if item.getDescription() == \"Chaos Bag\" then\n return item\n end\n end\n\n -- error handling: chaos bag not found\n printToAll(\"Chaos bag couldn't be found.\", \"Red\")\nend\n\nfunction returnChaosTokens()\n local chaosBag = findChaosBag()\n for _, token in pairs(chaosTokens) do\n if token ~= nil then chaosBag.putObject(token) end\n end\n chaosTokens = {}\nend\n\n-- returns a single chaos token to the bag and calls respective functions\nfunction returnChaosTokenToBag(token)\n local name = token.getName()\n local guid = token.getGUID()\n local chaosBag = findChaosBag()\n chaosBag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\nfunction getTokenIndex(token)\n for i, obj in ipairs(chaosTokens) do\n if obj == token then\n return i\n end\n end\nend\n\n-- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n-- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n-- contents of the bag should check this method before doing so.\n-- This method will broadcast a message to all players if the bag is being searched.\n---@return boolean: True if the bag is manipulated, false if it should be blocked.\nfunction canTouchChaosTokens()\n for _, searching in pairs(bagSearchers) do\n if searching then\n broadcastToAll(\"Someone is searching the chaos bag, can't touch the tokens.\", \"Red\")\n return false\n end\n end\n return true\nend\n\n-- converts the human readable name to the empty name that the bag uses\nfunction getChaosTokenName(tokenName)\n if tokenName == \"Custom Token\" then\n tokenName = \"\"\n end\n return tokenName\nend\n\n-- converts the empty name to the human readable name\nfunction getReadableTokenName(tokenName)\n if tokenName == \"\" then\n tokenName = \"Custom Token\"\n end\n return tokenName\nend\n\n-- called by playermats (by the \"Draw chaos token\" button)\nfunction drawChaosToken(params)\n if not canTouchChaosTokens() then return end\n\n local tokenOffset = { -1.55, 0.25, -0.58 }\n local matGUID = params.mat.getGUID()\n\n -- return token(s) on other playmat first\n if chaosTokensLastMatGUID ~= nil and chaosTokensLastMatGUID ~= matGUID and #chaosTokens ~= 0 then\n returnChaosTokens()\n chaosTokensLastMatGUID = nil\n return\n end\n\n chaosTokensLastMatGUID = matGUID\n\n -- if we have left clicked and have no tokens OR if we have right clicked\n if params.drawAdditional or #chaosTokens == 0 then\n local chaosBag = findChaosBag()\n if #chaosBag.getObjects() == 0 then return end\n chaosBag.shuffle()\n\n local indexOfReturnedToken\n local takeParameters = {}\n\n -- add the token to the list, compute new position based on list length\n if params.returnedToken then\n trackChaosToken(params.returnedToken.getName(), matGUID, true)\n indexOfReturnedToken = getTokenIndex(params.returnedToken)\n takeParameters.position = params.returnedToken.getPosition()\n if #chaosTokens \u003e indexOfReturnedToken then\n takeParameters.rotation = params.mat.getRotation() + Vector(0, 0, -8)\n else\n takeParameters.rotation = params.returnedToken.getRotation()\n end\n returnChaosTokenToBag(params.returnedToken)\n else\n tokenOffset[1] = tokenOffset[1] + (0.17 * #chaosTokens)\n takeParameters.position = params.mat.positionToWorld(tokenOffset)\n takeParameters.rotation = params.mat.getRotation()\n end\n\n local token\n if params.guidToBeResolved then\n -- resolve a sealed token from a card\n token = getObjectFromGUID(params.guidToBeResolved)\n token.setPositionSmooth(takeParameters.position)\n local guid = token.getGUID()\n local tokenType = token.getName()\n if tokenType == \"Bless\" or tokenType == \"Curse\" then\n blessCurseManagerApi.releasedToken(tokenType, guid)\n end\n tokenArrangerApi.layout()\n else\n -- take a token from the bag, either specified or random\n if params.tokenType then\n for i, lookedForToken in ipairs(chaosBag.getObjects()) do\n if lookedForToken.nickname == params.tokenType then\n takeParameters.index = i - 1\n end\n end\n end\n token = chaosBag.takeObject(takeParameters)\n end\n\n -- get data for token description\n local name = token.getName()\n local tokenData = mythosAreaApi.returnTokenData().tokenData or {}\n local specificData = tokenData[name] or {}\n token.setDescription(specificData.description or \"\")\n trackChaosToken(name, matGUID)\n\n if params.returnedToken then\n chaosTokens[indexOfReturnedToken] = token\n else\n chaosTokens[#chaosTokens + 1] = token\n end\n else\n returnChaosTokens()\n end\nend\n\n---------------------------------------------------------\n-- token spawning\n---------------------------------------------------------\n\n-- DEPRECATED. Use TokenManager instead.\n-- Spawns a single token.\n---@param params table Array with arguments to the method. 1 = position, 2 = type, 3 = rotation\nfunction spawnToken(params)\n return tokenManager.spawnToken(params[1], params[2], params[3])\nend\n\n---------------------------------------------------------\n-- chaos token stat tracker\n---------------------------------------------------------\n\nfunction trackChaosToken(tokenName, matGUID, subtract)\n -- initialize tables\n if not tokenDrawingStats[matGUID] then tokenDrawingStats[matGUID] = {} end\n\n -- increase stats by 1 (or decrease if token is returned)\n local modifier = (subtract and -1 or 1)\n\n local tokenName = getReadableTokenName(tokenName)\n tokenDrawingStats[\"Overall\"][tokenName] = (tokenDrawingStats[\"Overall\"][tokenName] or 0) + modifier\n tokenDrawingStats[matGUID][tokenName] = (tokenDrawingStats[matGUID][tokenName] or 0) + modifier\nend\n\n-- Left-click: print stats, Right-click: reset stats\nfunction handleStatTrackerClick(_, _, isRightClick)\n if isRightClick then\n resetChaosTokenStatTracker()\n else\n local squidKing = \"Nobody\"\n local maxSquid = 0\n local foundAnyStats = false\n\n for key, personalStats in pairs(tokenDrawingStats) do\n local playerColor, playerName\n\n if key == \"Overall\" then\n playerColor = \"White\"\n playerName = \"Overall\"\n else\n -- get mat color\n local matColor = playmatApi.getMatColorByPosition(getObjectFromGUID(key).getPosition())\n playerColor = playmatApi.getPlayerColor(matColor)\n playerName = Player[playerColor].steam_name or playerColor\n\n local playerSquidCount = personalStats[\"Auto-fail\"] or 0\n if playerSquidCount \u003e maxSquid then\n squidKing = playerName\n maxSquid = playerSquidCount\n end\n end\n\n -- get the total count of drawn tokens for the player\n local totalCount = 0\n for _, value in pairs(personalStats) do\n totalCount = totalCount + value\n end\n\n -- only print the personal stats if any tokens were drawn\n if totalCount \u003e 0 then\n foundAnyStats = true\n printToAll(\"------------------------------\")\n printToAll(playerName .. \" Stats\", playerColor)\n\n -- print stats in order of the \"ID_URL_MAP\"\n for _, subtable in pairs(ID_URL_MAP) do\n local tokenName = subtable.name\n local value = personalStats[tokenName]\n if value and value ~= 0 then\n printToAll(tokenName .. ': ' .. tostring(value))\n end\n end\n\n -- also print stats for custom tokens\n local customTokenName = getReadableTokenName(\"\")\n local customTokenCount = personalStats[customTokenName]\n if customTokenCount and customTokenCount ~= 0 then\n printToAll(customTokenName .. ': ' .. tostring(customTokenCount))\n end\n\n printToAll('Total: ' .. tostring(totalCount))\n end\n end\n\n -- detect if any player drew tokens\n if foundAnyStats then\n printToAll(\"------------------------------\")\n printToAll(squidKing .. \" is an auto-fail magnet.\", { 255, 0, 0 })\n else\n printToAll(\"No tokens have been drawn yet.\", \"Yellow\")\n end\n end\nend\n\n-- resets the count for each token to 0\nfunction resetChaosTokenStatTracker()\n tokenDrawingStats = { [\"Overall\"] = {} }\nend\n\n---------------------------------------------------------\n-- Difficulty selector script\n---------------------------------------------------------\n\n-- called for button creation on the difficulty selectors\n---@param args table Parameters for this function:\n-- object TTSObject Usually \"self\"\n-- key String Name of the scenario\nfunction createSetupButtons(args)\n local data = getDataValue('modeData', args.key)\n if data ~= nil then\n local buttonParameters = {}\n buttonParameters.function_owner = args.object\n buttonParameters.position = { 0, 0.1, -0.15 }\n buttonParameters.scale = { 0.47, 1, 0.47 }\n buttonParameters.height = 200\n buttonParameters.width = 1150\n buttonParameters.color = { 0.87, 0.8, 0.7 }\n\n if data.easy ~= nil then\n buttonParameters.label = \"Easy\"\n buttonParameters.click_function = \"easyClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.normal ~= nil then\n buttonParameters.label = \"Standard\"\n buttonParameters.click_function = \"normalClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.hard ~= nil then\n buttonParameters.label = \"Hard\"\n buttonParameters.click_function = \"hardClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.expert ~= nil then\n buttonParameters.label = \"Expert\"\n buttonParameters.click_function = \"expertClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.standalone ~= nil then\n buttonParameters.label = \"Standalone\"\n buttonParameters.click_function = \"standaloneClick\"\n args.object.createButton(buttonParameters)\n end\n end\nend\n\n-- called for adding chaos tokens\n---@param args table Parameters for this function:\n-- object object Usually \"self\"\n-- key string Name of the scenario\n-- mode string difficulty (e.g. \"hard\" or \"expert\")\nfunction fillContainer(args)\n local data = getDataValue('modeData', args.key)\n if data == nil then return end\n\n local value = data[args.mode]\n if value == nil or value.token == nil then return end\n\n local tokenList = {}\n\n for _, tokenId in ipairs(value.token) do\n table.insert(tokenList, tokenId)\n end\n\n if value.append ~= nil then\n for _, tokenId in ipairs(value.append) do\n table.insert(tokenList, tokenId)\n end\n end\n\n -- randomly choose tokens for specific Carcosa scenarios in standalone\n if value.random then\n local n = #value.random\n if n \u003e 0 then\n for _, tokenId in ipairs(value.random[math.random(1, n)]) do\n table.insert(tokenList, tokenId)\n end\n end\n end\n\n setChaosBagState(tokenList)\n\n if value.message then\n broadcastToAll(value.message)\n end\n\n if value.warning then\n broadcastToAll(value.warning, { 1, 0.5, 0.5 })\n end\nend\n\nfunction getDataValue(storage, key)\n local DATA_HELPER = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n local data = DATA_HELPER.getTable(storage)\n if data ~= nil then\n local value = data[key]\n if value ~= nil then\n local res = {}\n for m, v in pairs(value) do\n res[m] = v\n if res[m].parent ~= nil then\n local parentData = getDataValue(storage, res[m].parent)\n if parentData ~= nil and parentData[m] ~= nil and parentData[m].token ~= nil then\n res[m].token = parentData[m].token\n end\n res[m].parent = nil\n end\n end\n return res\n end\n end\nend\n\nfunction createChaosTokenNameLookupTable()\n local namesToIds = {}\n for k, v in pairs(ID_URL_MAP) do\n namesToIds[v.name] = k\n end\n return namesToIds\nend\n\n-- returns the currently drawn chaos tokens\n---@api ChaosBagApi\nfunction getChaosTokensinPlay()\n return chaosTokens\nend\n\n-- returns a Table List of chaos token ids in the current chaos bag\n---@api ChaosBag / ChaosBagApi\nfunction getChaosBagState()\n local tokens = {}\n local invertedTable = createChaosTokenNameLookupTable()\n local chaosBag = findChaosBag()\n\n for _, v in ipairs(chaosBag.getObjects()) do\n local id = invertedTable[v.name]\n if id then\n table.insert(tokens, id)\n else\n printToAll(v.name .. \" token not recognized. Will not be recorded.\", \"Yellow\")\n end\n end\n\n return tokens\nend\n\n-- respawns the chaos bag with a new state of tokens\n---@param tokenList table List of chaos token ids\n---@api ChaosBag / ChaosBagApi\nfunction setChaosBagState(tokenList)\n if not canTouchChaosTokens() then return end\n\n local chaosBag = findChaosBag()\n local chaosBagData = chaosBag.getData()\n local reserveData = getObjectFromGUID(\"106418\").getData()\n local tokenCache = {}\n local containedObjects = {}\n\n -- create a temporary copy of the data for each chaos token\n for _, objData in ipairs(reserveData.ContainedObjects) do\n tokenCache[objData.Nickname] = objData\n end\n\n -- iterate over tokenlist and insert specified tokens into new table\n for _, tokenId in ipairs(tokenList) do\n local tokenName = ID_URL_MAP[tokenId].name\n table.insert(containedObjects, tokenCache[tokenName])\n end\n\n -- overwrite chaos bag content and respawn it\n chaosBagData.ContainedObjects = containedObjects\n chaosBag.destruct()\n spawnObjectData({ data = chaosBagData })\n\n -- remove tokens that are still in play\n for _, token in pairs(chaosTokens) do\n if token ~= nil then token.destruct() end\n end\n chaosTokens = {}\n chaosTokensLastMatGUID = nil\n\n -- reset bless / curse manager\n blessCurseManagerApi.removeTakenTokensAndReset()\n\n printToAll(\"Chaos Bag set to chosen difficulty.\", \"Green\")\nend\n\n-- spawns the specified chaos token and puts it into the chaos bag\n---@param id string ID of the chaos token\nfunction spawnChaosToken(id)\n if not canTouchChaosTokens() then return end\n\n id = id:lower()\n local chaosBag = findChaosBag()\n local url = ID_URL_MAP[id].url or \"\"\n\n if url ~= \"\" then\n return spawnObject({\n type = 'Custom_Tile',\n position = { 0.49, 3, 0 },\n scale = { 0.81, 1.0, 0.81 },\n rotation = { 0, 270, 0 },\n callback_function = function(obj)\n obj.setName(ID_URL_MAP[id].name)\n chaosBag.putObject(obj)\n tokenArrangerApi.layout()\n end\n }).setCustomObject({\n type = 2,\n image = url,\n thickness = 0.1\n })\n end\nend\n\n-- removes the specified chaos token from the chaos bag\n---@param id string ID of the chaos token\nfunction removeChaosToken(id)\n if not canTouchChaosTokens() then return end\n\n local tokens = {}\n local chaosBag = findChaosBag()\n local name = ID_URL_MAP[id].name\n\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == name then table.insert(tokens, v.guid) end\n end\n\n -- error handling: no matching token found\n if #tokens == 0 then\n printToAll(\"No \" .. name .. \" tokens in the chaos bag.\", \"Yellow\")\n return\n end\n\n chaosBag.takeObject({\n guid = tokens[1],\n smooth = false,\n callback_function = function(obj)\n obj.destruct()\n tokenArrangerApi.layout()\n end\n })\n printToAll(\"Removing \" .. name .. \" token (in bag: \" .. #tokens - 1 .. \")\", \"White\")\nend\n\n-- returns all sealed tokens on cards to the chaos bag\nfunction releaseAllSealedTokens(playerColor)\n for _, obj in ipairs(getObjectsWithTag(\"CardThatSeals\")) do\n obj.call(\"releaseAllTokens\", playerColor)\n end\nend\n\n---------------------------------------------------------\n-- Content Importing and XML functions\n---------------------------------------------------------\n\n-- forwards the requested content type to the update function and sets highlight to clicked tab\n---@param tabId string Id of the clicked tab\nfunction onClick_tab(_, _, tabId)\n for listId, listContent in pairs(tabIdTable) do\n if listId == tabId then\n UI.setClass(listId, 'downloadTab activeTab')\n contentToShow = listContent\n else\n UI.setClass(listId, 'downloadTab')\n end\n end\n currentListItem = 1\n updateDownloadItemList()\nend\n\n-- click function for the items in the download window\n-- updates backgroundcolor for row panel and fontcolor for list item\nfunction onClick_select(_, _, identificationKey)\n UI.setAttribute(\"panel\" .. currentListItem, \"color\", \"clear\")\n UI.setAttribute(contentToShow .. \"_\" .. currentListItem, \"color\", \"white\")\n\n -- parses the identification key (contentToShow_currentListItem)\n if identificationKey then\n contentToShow = nil\n currentListItem = nil\n for str in string.gmatch(identificationKey, \"([^_]+)\") do\n if not contentToShow then\n -- grab the first part to know the content type\n contentToShow = str\n else\n -- get the index\n currentListItem = tonumber(str)\n break\n end\n end\n end\n\n UI.setAttribute(\"panel\" .. currentListItem, \"color\", \"grey\")\n UI.setAttribute(contentToShow .. \"_\" .. currentListItem, \"color\", \"black\")\n updatePreviewWindow()\nend\n\n-- click function for the \"Custom URL\" button in the playarea image gallery\nfunction onClick_customUrl(player)\n changeWindowVisibilityForColor(player.color, \"playareaGallery\")\n Wait.time(function()\n player.showInputDialog(\"Enter a custom URL for the playarea image\", \"\", function(newURL)\n playAreaApi.updateSurface(newURL)\n end)\n end, 0.15)\nend\n\n-- click function for the download button in the preview window\nfunction onClick_download(player)\n local params = library[contentToShow][currentListItem]\n params.player = player\n placeholder_download(params)\nend\n\n-- the download button on the placeholder objects calls this to directly initiate a download\n---@param params table contains url and guid of replacement object\nfunction placeholder_download(params)\n function downloadCoroutine()\n -- show progress bar\n UI.setAttribute('download_progress', 'active', true)\n\n -- update progress bar\n while requestObj do\n UI.setAttribute('download_progress', 'percentage', requestObj.download_progress * 100)\n coroutine.yield(0)\n end\n UI.setAttribute('download_progress', 'percentage', 100)\n\n -- wait 30 frames\n for i = 1, 30 do\n coroutine.yield(0)\n end\n\n -- hide progress bar\n UI.setAttribute('download_progress', 'active', false)\n\n -- hide download window\n if params.player then\n changeWindowVisibilityForColor(params.player.color, \"downloadWindow\", false)\n end\n return 1\n end\n\n local url = SOURCE_REPO .. '/' .. params.url\n requestObj = WebRequest.get(url, function(request) contentDownloadCallback(request, params) end)\n startLuaCoroutine(Global, 'downloadCoroutine')\nend\n\n-- spawns a bag that contains every object from the library\nfunction onClick_downloadAll(player)\n broadcastToAll(\"Download initiated - this will take a few minutes!\")\n\n -- hide download window\n changeWindowVisibilityForColor(player.color, \"downloadWindow\", false)\n\n startLuaCoroutine(Global, \"coroutineDownloadAll\")\nend\n\nfunction coroutineDownloadAll()\n local JSON = [[\n {\n \"Name\": \"Bag\",\n \"Transform\": {\n \"posX\": {{POSX}},\n \"posY\": 2,\n \"posZ\": -95,\n \"rotX\": 0,\n \"rotY\": 270,\n \"rotZ\": 0,\n \"scaleX\": 1,\n \"scaleY\": 1,\n \"scaleZ\": 1\n },\n \"Nickname\": \"{{NICKNAME}}\",\n \"Bag\": {\n \"Order\": 0\n },\n \"ContainedObjects\": [\n ]]\n\n local posx = -45.0\n local downloadedItems = 0\n local skippedItems = 0\n\n -- loop through the library to add content\n for contentType, objectList in pairs(library) do\n broadcastToAll(\"Downloading \" .. contentType .. \"...\")\n local contained = \"\"\n for _, params in ipairs(objectList) do\n local request = WebRequest.get(SOURCE_REPO .. '/' .. params.url, function() end)\n local start = os.time()\n while true do\n if request.is_done then\n contained = contained .. request.text .. \",\"\n downloadedItems = downloadedItems + 1\n break\n -- time-out if item can't be loaded in 5s\n elseif request.is_error or (os.time() - start) \u003e 5 then\n skippedItems = skippedItems + 1\n break\n end\n coroutine.yield(0)\n end\n end\n local JSONCopy = JSON\n JSONCopy = JSONCopy .. contained .. \"]}\"\n JSONCopy = JSONCopy:gsub(\"{{POSX}}\", posx)\n JSONCopy = JSONCopy:gsub(\"{{NICKNAME}}\", contentType)\n spawnObjectJSON({ json = JSONCopy })\n posx = posx + 3\n end\n\n broadcastToAll(downloadedItems .. \" objects downloaded.\", \"Green\")\n broadcastToAll(skippedItems .. \" objects had a time-out / error.\", \"Orange\")\n return 1\nend\n\n-- spawns a placeholder box for the selected object\nfunction onClick_spawnPlaceholder(player)\n -- get object references\n local item = library[contentToShow][currentListItem]\n local dummy = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlaceholderBoxDummy\")\n\n -- error handling\n if not item.boxsize or item.boxsize == \"\" or not item.boxart or item.boxart == \"\" then\n print(\"Error loading object.\")\n return\n end\n\n -- get data for placeholder\n local spawnPos = { -39.5, 2, -87 }\n\n local meshTable = {\n big = \"https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_MSH.obj\",\n small = \"https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/tuckbox_h_MSH.obj\",\n wide = \"http://cloud-3.steamusercontent.com/ugc/2278324073260846176/33EFCAF30567F8756F665BE5A2A6502E9C61C7F7/\"\n }\n\n local scaleTable = {\n big = { 1.00, 0.14, 1.00 },\n small = { 2.21, 0.46, 2.42 },\n wide = { 2.00, 0.11, 1.69 }\n }\n\n local placeholder = spawnObject({\n type = \"Custom_Model\",\n position = spawnPos,\n rotation = { 0, 270, 0 },\n scale = scaleTable[item.boxsize],\n })\n\n placeholder.setCustomObject({\n mesh = meshTable[item.boxsize],\n diffuse = item.boxart,\n material = 3\n })\n\n if item.boxsize == \"big\" then\n placeholder.addTag(\"LargeBox\")\n end\n\n placeholder.setColorTint({ 1, 1, 1, 71 / 255 })\n placeholder.setName(item.name)\n placeholder.setDescription(\"by \" .. (item.author or \"Unknown\"))\n placeholder.setGMNotes(item.url)\n placeholder.setLuaScript(dummy.getLuaScript())\n Player.getPlayers()[1].pingTable(spawnPos)\n\n -- hide download window\n changeWindowVisibilityForColor(player.color, \"downloadWindow\", false)\nend\n\n-- toggles the visibility of the respective UI\n---@param player tts__Player Player that triggered this\n---@param windowId string Name of the UI to toggle\nfunction onClick_toggleUi(player, windowId)\n if windowId == \"Navigation Overlay\" then\n navigationOverlayApi.cycleVisibility(player.color)\n return\n end\n\n -- hide the playAreaGallery if visible\n if windowId == \"downloadWindow\" then\n changeWindowVisibilityForColor(player.color, \"playAreaGallery\", false)\n -- hide the downloadWindow if visible\n elseif windowId == \"playAreaGallery\" then\n changeWindowVisibilityForColor(player.color, \"downloadWindow\", false)\n end\n\n changeWindowVisibilityForColor(player.color, windowId)\nend\n\n-- toggles the visibility of the specific window for the specified color\n---@param color string Player color to toggle the visibility for\n---@param windowId string ID of the XML element\n---@param overrideState? boolean Forcefully sets the new visibility\n---@return boolean visible Returns the new state of the visibility\nfunction changeWindowVisibilityForColor(color, windowId, overrideState)\n -- current state\n local colorString = UI.getAttribute(windowId, \"visibility\") or \"\"\n\n -- parse the visibility string\n local visible = false\n local viewers = {}\n for str in string.gmatch(colorString, \"%a+\") do\n table.insert(viewers, str)\n if str == color then\n visible = true\n end\n end\n\n -- add / remove the color as viewer\n if visible == true then\n removeValueFromTable(viewers, color)\n elseif visible == false then\n table.insert(viewers, color)\n end\n visible = not visible\n\n -- resolve override\n if overrideState == true and visible == false then\n table.insert(viewers, color)\n visible = true\n elseif overrideState == false and visible == true then\n removeValueFromTable(viewers, color)\n visible = false\n end\n\n -- construct new string\n local newColorString = \"\"\n for _, viewer in ipairs(viewers) do\n newColorString = newColorString .. viewer .. \"|\"\n end\n\n -- remove last delimiter\n newColorString = newColorString:sub(1, -2)\n\n -- update the visibility of the XML\n UI.setAttribute(windowId, \"visibility\", newColorString)\n UI.setAttribute(windowId, \"active\", newColorString ~= \"\")\n\n return visible\nend\n\n-- forwards the call to the onClick function\nfunction togglePlayAreaGallery(playerColor)\n changeWindowVisibilityForColor(playerColor, \"playareaGallery\")\nend\n\n-- updates the preview window\nfunction updatePreviewWindow()\n local item = library[contentToShow][currentListItem]\n local tempImage =\n \"http://cloud-3.steamusercontent.com/ugc/2115061845788345842/2CD6ABC551555CCF58F9D0DDB7620197BA398B06/\"\n\n -- set default image if not defined\n if item.boxsize == nil or item.boxsize == \"\" or item.boxart == nil or item.boxart == \"\" then\n item.boxsize = \"big\"\n item.boxart = \"http://cloud-3.steamusercontent.com/ugc/762723517667628371/18438B0A0045038A7099648AA3346DFCAA267C66/\"\n end\n\n UI.setValue(\"previewTitle\", item.name)\n UI.setValue(\"previewAuthor\", \"by \" .. (item.author or \"- Author not found -\"))\n UI.setValue(\"previewDescription\", item.description or \"- Description not found -\")\n\n -- update mask according to size (hardcoded values to align image in mask)\n local maskData = {}\n if item.boxsize == \"big\" then\n maskData = {\n image = \"box-cover-mask-big\",\n width = \"870\",\n height = \"435\",\n offsetXY = \"154 60\"\n }\n elseif item.boxsize == \"small\" then\n maskData = {\n image = \"box-cover-mask-small\",\n width = \"792\",\n height = \"594\",\n offsetXY = \"135 13\"\n }\n elseif item.boxsize == \"wide\" then\n maskData = {\n image = \"box-cover-mask-wide\",\n width = \"756\",\n height = \"630\",\n offsetXY = \"-190 -70\"\n }\n end\n\n -- loading empty image as placeholder until real image is loaded\n UI.setAttribute(\"previewArtImage\", \"image\", tempImage)\n\n -- insert the image itself\n UI.setAttribute(\"previewArtImage\", \"image\", item.boxart)\n UI.setAttributes(\"previewArtMask\", maskData)\nend\n\n-- formats the json response from the webrequest into a key-value lua table\n-- strips the prefix from the community content items\nfunction formatLibrary(json_response)\n library = {}\n library[\"campaigns\"] = json_response.campaigns\n library[\"scenarios\"] = json_response.scenarios\n library[\"extras\"] = json_response.extras\n library[\"fanmadeCampaigns\"] = {}\n library[\"fanmadeScenarios\"] = {}\n library[\"fanmadePlayerCards\"] = {}\n\n for _, item in ipairs(json_response.community) do\n local identifier = nil\n for str in string.gmatch(item.name, \"([^:]+)\") do\n if not identifier then\n -- grab the first part to know the content type\n identifier = str\n else\n -- update the name without the content type\n item.name = str\n break\n end\n end\n\n if identifier == \"Fan Investigators\" then\n table.insert(library[\"fanmadePlayerCards\"], item)\n elseif identifier == \"Fan Campaign\" then\n table.insert(library[\"fanmadeCampaigns\"], item)\n elseif identifier == \"Fan Scenario\" then\n table.insert(library[\"fanmadeScenarios\"], item)\n end\n end\nend\n\n-- updates the window content to the requested content\nfunction updateDownloadItemList()\n if not library then return end\n\n -- addition of list items according to library file\n local globalXml = UI.getXmlTable()\n local contentList = getXmlTableElementById(globalXml, 'contentList')\n\n contentList.children = {}\n for i, v in ipairs(library[contentToShow]) do\n table.insert(contentList.children,\n {\n tag = \"Panel\",\n attributes = { id = \"panel\" .. i },\n children = {\n tag = 'Text',\n value = v.name,\n attributes = {\n id = contentToShow .. \"_\" .. i,\n onClick = 'onClick_select',\n alignment = 'MiddleLeft'\n }\n }\n })\n end\n\n contentList.attributes.height = #contentList.children * 27\n updateGlobalXml(globalXml)\n\n -- select the first item\n Wait.time(onClick_select, 0.2)\nend\n\n-- this helper function updates the global XML while preserving the visibility of windows\nfunction updateGlobalXml(newXml)\n -- preserve visibility settings for these elements\n local windowIdList = {\n \"playAreaGallery\",\n \"downloadWindow\",\n \"optionPanel\"\n }\n\n -- get current state and update newXml\n for _, windowId in ipairs(windowIdList) do\n local element = getXmlTableElementById(newXml, windowId)\n element.attributes.active = UI.getAttribute(windowId, \"active\")\n element.attributes.visibility = UI.getAttribute(windowId, \"visibility\")\n end\n\n UI.setXmlTable(newXml)\nend\n\n-- called after the webrequest of downloading an item\n-- deletes the placeholder and spawns the downloaded item\nfunction contentDownloadCallback(request, params)\n requestObj = nil\n\n -- error handling\n if request.is_error or request.response_code ~= 200 then\n print('Error: ' .. request.error)\n return\n end\n\n -- initiate content spawning\n local spawnTable = { json = request.text }\n if params.replace then\n local replacedObject = getObjectFromGUID(params.replace)\n if replacedObject then\n spawnTable.position = replacedObject.getPosition()\n spawnTable.rotation = replacedObject.getRotation()\n spawnTable.scale = replacedObject.getScale()\n destroyObject(replacedObject)\n end\n end\n\n -- if position is undefined, get empty position\n if not spawnTable.position then\n spawnTable.rotation = { 0, 270, 0 }\n\n local pos = getValidSpawnPosition()\n if pos then\n spawnTable.position = pos\n else\n broadcastToAll(\"Please make space in the area below the tentacle stand in the upper middle of the table and try again.\", \"Red\")\n return\n end\n end\n\n -- if spawned from menu, move the camera and/or ping the table\n if params.name then\n spawnTable[\"callback_function\"] = function(obj)\n Wait.time(function()\n -- move camera\n if params.player then\n params.player.lookAt({\n position = obj.getPosition(),\n pitch = 65,\n yaw = 90,\n distance = 65\n })\n end\n\n -- ping object\n local pingPlayer = params.player or Player.getPlayers()[1]\n pingPlayer.pingTable(obj.getPosition())\n end, 0.1)\n end\n end\n\n if pcall(function() spawnObjectJSON(spawnTable) end) then\n print('Object loaded.')\n else\n print('Error loading object.')\n end\nend\n\n-- gets the first empty position to spawn a custom content object safely\nfunction getValidSpawnPosition()\n local potentialSpawnPositionX = { 65, 50, 35 }\n local potentialSpawnPositionY = 1.5\n local potentialSpawnPositionZ = { 35, 21, 7, -7, -21, -35 }\n\n for i, posX in ipairs(potentialSpawnPositionX) do\n for j, posZ in ipairs(potentialSpawnPositionZ) do\n local pos = {\n x = posX,\n y = potentialSpawnPositionY,\n z = posZ,\n }\n if checkPositionForContentSpawn(pos) then\n return pos\n end\n end\n end\n return nil\nend\n\n-- checks whether something is in the specified position\n-- returns true if empty\nfunction checkPositionForContentSpawn(checkPos)\n local searchResult = searchLib.atPosition(checkPos)\n\n -- first hit is the table surface, additional hits means something is there\n return #searchResult == 1\nend\n\n-- downloading of the library file\nfunction libraryDownloadCallback(request)\n if request.is_error or request.response_code ~= 200 then\n print('error: ' .. request.error)\n return\n end\n\n local json_response = nil\n if pcall(function() json_response = JSON.decode(request.text) end) then\n formatLibrary(json_response)\n updateDownloadItemList()\n else\n print('error parsing downloaded library')\n end\nend\n\n-- loops through an XML table and returns the specified object\n---@param ui table XmlTable (get this via getXmlTable)\n---@param id string Id of the object to return\nfunction getXmlTableElementById(ui, id)\n for _, obj in ipairs(ui) do\n if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end\n if obj.children then\n local result = getXmlTableElementById(obj.children, id)\n if result then return result end\n end\n end\n return nil\nend\n\n---------------------------------------------------------\n-- Option Panel related functionality\n---------------------------------------------------------\n\n-- changes the UI state and the internal variable for the togglebuttons\nfunction onClick_toggleOption(_, _, id)\n local currentState = optionPanel[id]\n local newState = not currentState\n applyOptionPanelChange(id, newState)\n UI.setAttribute(id, \"image\", newState and \"option-on\" or \"option-off\")\nend\n\n-- color selection for playArea\nfunction onClick_playAreaConnectionColor(player, _, id)\n player.showColorDialog(optionPanel[id], function(color)\n applyOptionPanelChange(id, color)\n end)\nend\n\n-- called by the language selection dropdown\nfunction languageSelected(_, selectedIndex, id)\n optionPanel[id] = LANGUAGES[tonumber(selectedIndex) + 1].code\nend\n\n-- returns the ID (position in the table) for a provided language code\nfunction returnLanguageId(code)\n for index, tbl in ipairs(LANGUAGES) do\n if tbl.code == code then\n return index\n end\n end\nend\n\n-- called by the resource counter selection dropdown\nfunction resourceCounterSelected(_, selectedIndex, id)\n optionPanel[id] = RESOURCE_OPTIONS[tonumber(selectedIndex) + 1]\nend\n\n-- returns the ID for the provided option name\nfunction returnResourceCounterId(name)\n for index, optionName in ipairs(RESOURCE_OPTIONS) do\n if optionName == name then\n return index\n end\n end\nend\n\n-- called by the playermat removal selection dropdown\nfunction playermatRemovalSelected(player, selectedIndex, id)\n if selectedIndex == \"0\" then return end\n\n local matColorList = { \"White\", \"Orange\", \"Green\", \"Red\" }\n local matColor = matColorList[tonumber(selectedIndex)]\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n if mat then\n -- confirmation dialog about deletion\n player.pingTable(mat.getPosition())\n player.showConfirmDialog(\"Do you really want to remove \" .. matColor .. \"'s playermat and related objects? This can't be reversed.\", function() removePlayermat(matColor) end)\n else\n -- info dialog that it is already deleted\n player.showInfoDialog(matColor .. \"'s playermat has already been removed.\")\n end\n\n -- set selected value back to first option\n UI.setAttribute(id, \"value\", 0)\nend\n\n-- removes a playermat and all related objects from play\n---@param matColor string Color of the playermat to remove\nfunction removePlayermat(matColor)\n local matObjects = guidReferenceApi.getObjectsByOwner(matColor)\n if not matObjects.Playermat then return end\n\n -- remove action tokens\n local actionTokens = playmatApi.searchAroundPlaymat(matColor, \"isActionToken\")\n for _, obj in ipairs(actionTokens) do\n obj.destruct()\n end\n\n -- remove mat owned objects\n for _, obj in pairs(matObjects) do\n obj.destruct()\n end\nend\n\n-- sets the option panel to the correct state (corresponding to 'optionPanel')\nfunction updateOptionPanelState()\n for id, optionValue in pairs(optionPanel) do\n if id == \"cardLanguage\" and type(optionValue) == \"string\" then\n local dropdownId = returnLanguageId(optionValue) - 1\n UI.setAttribute(id, \"value\", dropdownId)\n elseif id == \"useResourceCounters\" and type(optionValue) == \"string\" then\n local dropdownId = returnResourceCounterId(optionValue) - 1\n UI.setAttribute(id, \"value\", dropdownId)\n elseif id == \"playAreaConnectionColor\" then\n UI.setAttribute(id, \"color\", \"#\" .. Color.new(optionValue):toHex())\n elseif (type(optionValue) == \"boolean\" and optionValue)\n or (type(optionValue) == \"string\" and optionValue)\n or (type(optionValue) == \"table\" and #optionValue ~= 0) then\n UI.setAttribute(id, \"image\", \"option-on\")\n else\n UI.setAttribute(id, \"image\", \"option-off\")\n end\n end\nend\n\n-- handles the applying of option selections and calls the respective functions based on the id\n---@param id string ID of the option that was selected or deselected\n---@param state boolean|any State of the option (true = enabled)\nfunction applyOptionPanelChange(id, state)\n optionPanel[id] = state\n\n -- option: Snap tags\n if id == \"useSnapTags\" then\n playmatApi.setLimitSnapsByType(state, \"All\")\n\n -- option: Draw 1 button\n elseif id == \"showDrawButton\" then\n playmatApi.showDrawButton(state, \"All\")\n\n -- option: Clickable clue counters\n elseif id == \"useClueClickers\" then\n playmatApi.clickableClues(state, \"All\")\n\n -- update master clue counter\n local counter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MasterClueCounter\")\n counter.setVar(\"useClickableCounters\", state)\n\n -- option: Play area snap tags\n elseif id == \"playAreaConnections\" then\n playAreaApi.setConnectionDrawState(state)\n\n -- option: Play area connection color\n elseif id == \"playAreaConnectionColor\" then\n playAreaApi.setConnectionColor(state)\n UI.setAttribute(id, \"color\", \"#\" .. Color.new(state):toHex())\n\n -- option: Play area snap tags\n elseif id == \"playAreaSnapTags\" then\n playAreaApi.setLimitSnapsByType(state)\n\n -- option: Show clean up helper\n elseif id == \"showCleanUpHelper\" then\n spawnOrRemoveHelper(state, \"Clean Up Helper\", { -66, 1.6, 46 })\n\n -- option: Show hand helper for each player\n elseif id == \"showHandHelper\" then\n spawnOrRemoveHelperForPlayermats(\"Hand Helper\", state)\n\n -- option: Show search assistant for each player\n elseif id == \"showSearchAssistant\" then\n spawnOrRemoveHelperForPlayermats(\"Search Assistant\", state)\n\n -- option: Show attachment helper\n elseif id == \"showAttachmentHelper\" then\n spawnOrRemoveHelper(state, \"Attachment Helper\", { -62, 1.4, 0 })\n\n -- option: Show CYOA campaign guides\n elseif id == \"showCYOA\" then\n spawnOrRemoveHelper(state, \"CYOA Campaign Guides\", { 39, 1.3, -20 })\n\n -- option: Show displacement tool\n elseif id == \"showDisplacementTool\" then\n spawnOrRemoveHelper(state, \"Displacement Tool\", { -57, 1.6, 46 })\n end\nend\n\n-- spawns or removes a helper object for all playermats\n---@param helperName string Name of the helper object\n---@param state boolean Contains the state of the option: true = spawn it, false = remove it\nfunction spawnOrRemoveHelperForPlayermats(helperName, state)\n for color, data in pairs(playmatApi.getHelperSpawnData(\"All\", helperName)) do\n spawnOrRemoveHelper(state, helperName, data.position, data.rotation, color)\n end\nend\n\n-- handler for spawn / remove functions of helper objects\n---@param state boolean Contains the state of the option: true = spawn it, false = remove it\n---@param name string Name of the helper object\n---@param position tts__Vector Position of the object (where it will spawn)\n---@param rotation? tts__Vector Rotation of the object for spawning (default: {0, 270, 0})\n---@param owner? string Owner of the object (defaults to \"Mythos\")\n---@return string|nil GUID GUID of the spawnedObj (or nil if object was removed)\nfunction spawnOrRemoveHelper(state, name, position, rotation, owner)\n if state then\n Player.getPlayers()[1].pingTable(position)\n local spawnedGUID = spawnHelperObject(name, position, rotation).getGUID()\n local cleanName = name:gsub(\"%s+\", \"\")\n guidReferenceApi.editIndex(owner or \"Mythos\", cleanName, spawnedGUID)\n else\n removeHelperObject(name)\n end\nend\n\n-- copies the specified tool (by name) from the option panel source bag\n---@param name string Name of the object that should be copied\n---@param position tts__Vector Desired position of the object\n---@param rotation? tts__Vector Desired rotation of the object (defaults to object's rotation)\nfunction spawnHelperObject(name, position, rotation)\n local sourceBag = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"OptionPanelSource\")\n\n -- error handling for missing sourceBag\n if not sourceBag then\n broadcastToAll(\"Option panel source bag could not be found!\", \"Red\")\n return\n end\n\n local spawnTable = { position = position }\n\n -- only overrride rotation if there is one provided (object's rotation used instead)\n if rotation then\n spawnTable.rotation = rotation\n end\n\n for _, obj in ipairs(sourceBag.getData().ContainedObjects) do\n if obj[\"Nickname\"] == name then\n spawnTable.data = obj\n spawnTable.callback_function = function(spawnedObj)\n Wait.time(function() spawnedObj.setLock(true) end, 2)\n end\n return spawnObjectData(spawnTable)\n end\n end\nend\n\n-- removes the specified tool (by name)\n---@param name string Object that should be removed\nfunction removeHelperObject(name)\n local cleanName = name:gsub(\"%s+\", \"\")\n for _, obj in pairs(guidReferenceApi.getObjectsByType(cleanName)) do\n obj.destruct()\n end\nend\n\n-- loads saved options\n---@param newOptions table Contains the new state for the option panel\nfunction loadSettings(newOptions)\n for id, state in pairs(newOptions) do\n if optionPanel[id] ~= state then\n optionPanel[id] = state\n applyOptionPanelChange(id, state)\n end\n end\n\n -- update XML UI state\n updateOptionPanelState()\nend\n\n-- loads the default options\nfunction onClick_defaultSettings()\n -- clean reset of variables\n optionPanel = {\n cardLanguage = \"en\",\n changePlayAreaImage = false,\n playAreaConnectionColor = { a = 1, b = 0.4, g = 0.4, r = 0.4 },\n playAreaConnections = true,\n playAreaSnapTags = true,\n showAttachmentHelper = false,\n showCleanUpHelper = false,\n showCYOA = false,\n showDisplacementTool = false,\n showDrawButton = false,\n showHandHelper = false,\n showSearchAssistant = false,\n showTitleSplash = true,\n useClueClickers = false,\n useResourceCounters = \"disabled\",\n useSnapTags = true\n }\n\n -- applying changes\n for id, state in pairs(optionPanel) do\n applyOptionPanelChange(id, state)\n end\n\n -- update UI\n updateOptionPanelState()\nend\n\n-- splash scenario title on setup\nfunction titleSplash(scenarioName)\n if optionPanel['showTitleSplash'] then\n -- if there's any ongoing title being displayed, hide it and cancel the waiting function\n if hideTitleSplashWaitFunctionId then\n Wait.stop(hideTitleSplashWaitFunctionId)\n hideTitleSplashWaitFunctionId = nil\n UI.setAttribute('title_splash', 'active', false)\n end\n\n -- display scenario name and set a 4 seconds (2 seconds animation and 2 seconds on screen)\n -- wait timer to hide the scenario name\n UI.setValue('title_splash_text', scenarioName)\n UI.show('title_splash')\n hideTitleSplashWaitFunctionId = Wait.time(function()\n UI.hide('title_splash')\n hideTitleSplashWaitFunctionId = nil\n end, 4)\n\n soundCubeApi.playSoundByName(\"Deep Bell\")\n end\nend\n\n---------------------------------------------------------\n-- Update notification related functionality\n---------------------------------------------------------\n\n-- grabs the latest mod version and release notes from GitHub (called onLoad())\nfunction getModVersion()\n WebRequest.get(SOURCE_REPO .. '/modversion.json', compareVersion)\nend\n\n-- compares the modversion with GitHub and possibly shows the update notification\nfunction compareVersion(request)\n if request.is_error then\n log(request.error)\n return\n end\n\n -- global variable to make it accessible for other functions\n modMeta = JSON.decode(request.text)\n\n -- stop here if on latest or newer version\n if convertVersionToNumber(MOD_VERSION) \u003e= convertVersionToNumber(modMeta[\"latestVersion\"]) then return end\n\n -- stop here if \"don't show again\" was clicked for this version before\n if acknowledgedUpgradeVersions[modMeta[\"latestVersion\"]] then return end\n\n updateNotificationLoading()\n\n -- delay to avoid lagging during onLoad()\n Wait.time(function() UI.show(\"FinnIcon\") end, 1)\nend\n\n-- converts a version number to a string\n---@param version string Version number, separated by dots (e.g. 3.3.1)\nfunction convertVersionToNumber(version)\n local major, minor, patch = string.match(version, \"(%d+)%.(%d+)%.(%d+)\")\n return major * 100 + minor * 10 + patch\nend\n\n-- updates the XML update notification based on the mod metadata\nfunction updateNotificationLoading()\n -- grab data\n local highlights = modMeta[\"releaseHighlights\"]\n\n -- concatenate the release highlights\n local highlightText = \"• \" .. highlights[1]\n for i, entry in pairs(highlights) do\n if i ~= 1 then\n highlightText = highlightText .. \"\\n• \" .. entry\n end\n end\n\n -- update the XML UI\n UI.setValue(\"notificationHeader\", \"New version available: \" .. modMeta[\"latestVersion\"])\n UI.setValue(\"releaseHighlightText\", highlightText)\n UI.setAttribute(\"highlightRow\", \"preferredHeight\", 20 * #highlights)\n UI.setAttribute(\"updateNotification\", \"height\", 20 * #highlights + 125)\nend\n\n-- close / don't show again buttons on the update notification\nfunction onClick_notification(_, parameter)\n if parameter == \"dontShowAgain\" then\n -- this variable tracks if \"don't show again\" was pressed for a version\n acknowledgedUpgradeVersions[modMeta[\"latestVersion\"]] = true\n end\n UI.hide(\"FinnIcon\")\n UI.hide(\"updateNotification\")\nend\n\n---------------------------------------------------------\n-- Utility functions\n---------------------------------------------------------\n\nfunction removeValueFromTable(t, val)\n for i, v in ipairs(t) do\n if v == val then\n table.remove(t, i)\n break\n end\n end\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/Global\")\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/SoundCubeApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SoundCubeApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- this table links the name of a trigger effect to its index\n local soundIndices = {\n [\"Vacuum\"] = 0,\n [\"Deep Bell\"] = 1,\n [\"Dark Souls\"] = 2\n }\n\n ---@param index number Index of the sound effect to play\n local function playTriggerEffect(index)\n local SoundCube = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"SoundCube\")\n SoundCube.AssetBundle.playTriggerEffect(index)\n end\n\n -- plays the by name requested sound\n ---@param soundName string Name of the sound to play\n SoundCubeApi.playSoundByName = function(soundName)\n playTriggerEffect(soundIndices[soundName])\n end\n\n return SoundCubeApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "{\"acknowledgedUpgradeVersions\":[],\"chaosTokensGUID\":[],\"optionPanel\":{\"cardLanguage\":\"en\",\"changePlayAreaImage\":false,\"playAreaConnectionColor\":{\"a\":1,\"b\":0.4,\"g\":0.4,\"r\":0.4},\"playAreaConnections\":true,\"playAreaSnapTags\":true,\"showAttachmentHelper\":false,\"showCleanUpHelper\":false,\"showCYOA\":false,\"showDisplacementTool\":false,\"showDrawButton\":false,\"showHandHelper\":false,\"showSearchAssistant\":false,\"showTitleSplash\":true,\"useClueClickers\":false,\"useResourceCounters\":\"disabled\",\"useSnapTags\":true}}", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/Global\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal soundCubeApi = require(\"core/SoundCubeApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n---------------------------------------------------------\n-- general setup\n---------------------------------------------------------\n\nENCOUNTER_DECK_POS = { -3.93, 1, 5.76 }\nENCOUNTER_DECK_DISCARD_POSITION = { -3.85, 1, 10.38 }\n\n-- GUIDs that will not be interactable (e.g. parts of the table)\nlocal NOT_INTERACTABLE = {\n \"6161b4\", -- Decoration-Map\n \"9f334f\", -- MythosArea\n \"463022\", -- Panel behind tentacle stand\n \"f182ee\", -- InvestigatorCount\n \"7bff34\", -- Tentacle stand\n \"8646eb\", -- horizontal border left\n \"75937e\", -- horizontal border right\n \"612072\", -- vertical border left\n \"975c39\", -- vertical border right\n}\n\nlocal chaosTokens = {}\nlocal chaosTokensLastMatGUID = nil\n\n-- chaos token stat tracking\nlocal tokenDrawingStats = { [\"Overall\"] = {} }\n\nlocal bagSearchers = {}\nlocal hideTitleSplashWaitFunctionId = nil\n\n-- online functionality related variables\nlocal MOD_VERSION = \"3.9.0\"\nlocal SOURCE_REPO = 'https://raw.githubusercontent.com/chr1z93/loadable-objects/main'\nlocal library, requestObj, modMeta\nlocal acknowledgedUpgradeVersions = {}\nlocal contentToShow = \"campaigns\"\nlocal currentListItem = 1\nlocal tabIdTable = {\n tab1 = \"campaigns\",\n tab2 = \"scenarios\",\n tab3 = \"fanmadeCampaigns\",\n tab4 = \"fanmadeScenarios\",\n tab5 = \"fanmadePlayerCards\"\n}\n\n-- optionPanel data (intentionally not local!)\noptionPanel = {}\nlocal LANGUAGES = {\n { code = \"zh_CN\", name = \"简体中文\" },\n { code = \"zh_TW\", name = \"繁體中文\" },\n { code = \"de\", name = \"Deutsch\" },\n { code = \"en\", name = \"English\" },\n { code = \"es\", name = \"Español\" },\n { code = \"fr\", name = \"Français\" },\n { code = \"it\", name = \"Italiano\" }\n}\nlocal RESOURCE_OPTIONS = {\n \"enabled\",\n \"custom\",\n \"disabled\"\n}\n\n---------------------------------------------------------\n-- data for tokens\n---------------------------------------------------------\n\nTOKEN_DATA = {\n damage = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357115146/903D11AAE7BD5C254C8DC136E9202EE516289DEA/\", scale = { 0.17, 0.17, 0.17 } },\n horror = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357163535/6D9E0756503664D65BDB384656AC6D4BD713F5FC/\", scale = { 0.17, 0.17, 0.17 } },\n resource = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/\", scale = { 0.17, 0.17, 0.17 } },\n doom = { image = \"https://i.imgur.com/EoL7yaZ.png\", scale = { 0.17, 0.17, 0.17 } },\n clue = { image = \"http://cloud-3.steamusercontent.com/ugc/1758068501357164917/1D06F1DC4D6888B6F57124BD2AFE20D0B0DA15A8/\", scale = { 0.15, 0.15, 0.15 } }\n}\n\nID_URL_MAP = {\n ['blue'] = { name = \"Elder Sign\", url = 'https://i.imgur.com/nEmqjmj.png' },\n ['p1'] = { name = \"+1\", url = 'https://i.imgur.com/uIx8jbY.png' },\n ['0'] = { name = \"0\", url = 'https://i.imgur.com/btEtVfd.png' },\n ['m1'] = { name = \"-1\", url = 'https://i.imgur.com/w3XbrCC.png' },\n ['m2'] = { name = \"-2\", url = 'https://i.imgur.com/bfTg2hb.png' },\n ['m3'] = { name = \"-3\", url = 'https://i.imgur.com/yfs8gHq.png' },\n ['m4'] = { name = \"-4\", url = 'https://i.imgur.com/qrgGQRD.png' },\n ['m5'] = { name = \"-5\", url = 'https://i.imgur.com/3Ym1IeG.png' },\n ['m6'] = { name = \"-6\", url = 'https://i.imgur.com/c9qdSzS.png' },\n ['m7'] = { name = \"-7\", url = 'https://i.imgur.com/4WRD42n.png' },\n ['m8'] = { name = \"-8\", url = 'https://i.imgur.com/9t3rPTQ.png' },\n ['skull'] = { name = \"Skull\", url = 'https://i.imgur.com/stbBxtx.png' },\n ['cultist'] = { name = \"Cultist\", url = 'https://i.imgur.com/VzhJJaH.png' },\n ['tablet'] = { name = \"Tablet\", url = 'https://i.imgur.com/1plY463.png' },\n ['elder'] = { name = \"Elder Thing\", url = 'https://i.imgur.com/ttnspKt.png' },\n ['red'] = { name = \"Auto-fail\", url = 'https://i.imgur.com/lns4fhz.png' },\n ['bless'] = { name = \"Bless\", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778627699/339FB716CB25CA6025C338F13AFDFD9AC6FA8356/' },\n ['curse'] = { name = \"Curse\", url = 'http://cloud-3.steamusercontent.com/ugc/1655601092778636039/2A25BD38E8C44701D80DD96BF0121DA21843672E/' },\n ['frost'] = { name = \"Frost\", url = 'http://cloud-3.steamusercontent.com/ugc/1858293462583104677/195F93C063A8881B805CE2FD4767A9718B27B6AE/' }\n}\n\n---------------------------------------------------------\n-- general code\n---------------------------------------------------------\n\n-- saving state of optionPanel to restore later\nfunction onSave()\n local chaosTokensGUID = {}\n for _, obj in ipairs(chaosTokens) do\n if obj ~= nil then\n table.insert(chaosTokensGUID, obj.getGUID())\n end\n end\n\n return JSON.encode({\n optionPanel = optionPanel,\n acknowledgedUpgradeVersions = acknowledgedUpgradeVersions,\n chaosTokensLastMatGUID = chaosTokensLastMatGUID,\n chaosTokensGUID = chaosTokensGUID\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n optionPanel = loadedData.optionPanel\n acknowledgedUpgradeVersions = loadedData.acknowledgedUpgradeVersions\n chaosTokensLastMatGUID = loadedData.chaosTokensLastMatGUID\n\n -- restore saved state for drawn chaos tokens\n for _, guid in ipairs(loadedData.chaosTokensGUID or {}) do\n table.insert(chaosTokens, getObjectFromGUID(guid))\n end\n\n updateOptionPanelState()\n end\n\n for _, guid in ipairs(NOT_INTERACTABLE) do\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then obj.interactable = false end\n end\n\n getModVersion()\n math.randomseed(os.time())\n\n -- initialization of loadable objects library (delay to let Navigation Overlay build)\n Wait.time(function()\n WebRequest.get(SOURCE_REPO .. '/library.json', libraryDownloadCallback)\n end, 1)\nend\n\n-- provides a random seed (from 1 to 999) to be used by \"linked\" objects like the action tokens\nfunction getRandomSeed()\n return math.random(999)\nend\n\n-- Event hook for any object search. When chaos tokens are manipulated while the chaos bag\n-- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the\n-- chaos bag during search operations to avoid this.\nfunction onObjectSearchStart(object, playerColor)\n local chaosBag = findChaosBag()\n if object == chaosBag then\n bagSearchers[playerColor] = true\n end\nend\n\n-- Event hook for any object search. When chaos tokens are manipulated while the chaos bag\n-- container is being searched, a TTS bug can cause tokens to duplicate or vanish. We lock the\n-- chaos bag during search operations to avoid this.\nfunction onObjectSearchEnd(object, playerColor)\n local chaosBag = findChaosBag()\n if object == chaosBag then\n bagSearchers[playerColor] = nil\n end\n Player[playerColor].clearSelectedObjects()\nend\n\n-- Pass object enter container events to the PlayArea to clear vector lines from dragged cards.\n-- This requires the try method as cards won't exist any more after they enter a deck, so the lines\n-- can't be cleared.\nfunction tryObjectEnterContainer(container, object)\n -- stop mini cards from forming decks\n if object.hasTag(\"Minicard\") and container.hasTag(\"Minicard\") then\n return false\n end\n\n playAreaApi.tryObjectEnterContainer(container, object)\n return true\nend\n\n-- TTS event for objects that enter zones\nfunction onObjectEnterZone(zone, object)\n -- detect the \"token discard zones\" beneath the hand zones\n if zone.getName() == \"TokenDiscardZone\" and\n not tokenChecker.isChaosToken(object) and\n object.type == \"Tile\" and\n object.getMemo() and\n not object.getLock() then\n local matcolor = playermatApi.getMatColorByPosition(object.getPosition())\n local trash = guidReferenceApi.getObjectByOwnerAndType(matcolor, \"Trash\")\n trash.putObject(object)\n elseif zone.type == \"Hand\" and object.type == \"Card\" then\n -- make sure the card is face-up\n if object.is_face_down then\n object.flip()\n end\n\n -- disable any helpers on the card\n if object.hasTag(\"CardWithHelper\") then\n object.call(\"setHelperState\", false)\n end\n\n -- maybe reset data about sealed tokens (if that function exists)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\")\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n end\nend\n\n-- TTS event for objects that leave zones\nfunction onObjectLeaveZone(zone, object)\n -- 1 frame delay to avoid error messages when exiting the game\n Wait.frames(\n function()\n -- end here if one of the objects doesn't exist\n if zone.isDestroyed() or object.isDestroyed() then return end\n\n -- resync the state of the helper on the card with the option panel\n if zone.type == \"Hand\" and object.hasTag(\"CardWithHelper\") then\n object.call(\"syncDisplayWithOptionPanel\")\n end\n end, 1)\nend\n\n-- handle card drawing via number typing for multihanded gameplay\n-- (and additionally allow Norman Withers to draw multiple cards via number)\nfunction onObjectNumberTyped(hoveredObject, playerColor, number)\n -- only continue for decks or cards\n if hoveredObject.type ~= \"Deck\" and hoveredObject.type ~= \"Card\" then return end\n\n -- check if this is a card with states (and then change state instead of drawing it)\n local states = hoveredObject.getStates()\n if states ~= nil and #states \u003e 0 then\n local stateId = hoveredObject.getStateId()\n if stateId ~= number and (#states + 1) \u003e= number then\n hoveredObject.setState(number)\n return true\n end\n end\n\n -- check whether the hovered object is part of a players draw objects\n for _, color in ipairs(playermatApi.getUsedMatColors()) do\n local deckAreaObjects = playermatApi.getDeckAreaObjects(color)\n if deckAreaObjects.topCard == hoveredObject or deckAreaObjects.draw == hoveredObject then\n playermatApi.drawCardsWithReshuffle(color, number)\n return true\n end\n end\nend\n\n-- TTS event, used to redraw the playermat slot symbols after a small delay to account for the custom font loading\nfunction onPlayerConnect()\n Wait.time(function() playermatApi.redrawSlotSymbols(\"All\") end, 0.2)\nend\n\n-- disable delete action (only applies to promoted players) and discard objects instead\nfunction onPlayerAction(player, action, targets)\n if action == Player.Action.Delete and not player.admin then\n for _, target in ipairs(targets) do\n local matColor = playermatApi.getMatColorByPosition(target.getPosition())\n local trash = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Trash\")\n trash.putObject(target)\n end\n return false\n end\n return true\nend\n\n---------------------------------------------------------\n-- chaos token drawing\n---------------------------------------------------------\n\n-- checks scripting zone for chaos bag (also called by a lot of objects!)\nfunction findChaosBag()\n local chaosBagZone = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"ChaosBagZone\")\n\n -- error handling: scripting zone not found\n if chaosBagZone == nil then\n printToAll(\"Zone for chaos bag detection couldn't be found.\", \"Red\")\n return\n end\n\n for _, item in ipairs(chaosBagZone.getObjects()) do\n if item.getDescription() == \"Chaos Bag\" then\n return item\n end\n end\n\n -- error handling: chaos bag not found\n printToAll(\"Chaos bag couldn't be found.\", \"Red\")\nend\n\n-- returns all chaos tokens to the bag\nfunction returnChaosTokens()\n local chaosBag = findChaosBag()\n for _, token in pairs(chaosTokens) do\n if token ~= nil then chaosBag.putObject(token) end\n end\n chaosTokens = {}\n isTokenXMLActive = false\nend\n\n-- returns a single chaos token to the bag and calls respective functions\nfunction returnChaosTokenToBag(params)\n local name = params.token.getName()\n local chaosBag = findChaosBag()\n chaosBag.putObject(params.token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, params.token.getGUID(), params.fromBag)\n end\nend\n\n-- returns the index of a token in the chaosTokens table\nfunction getTokenIndex(token)\n for i, obj in ipairs(chaosTokens) do\n if obj == token then\n return i\n end\n end\nend\n\n-- starts a redraw effect and displays buttons for a choice if needed\nfunction activeRedrawEffect(params)\n redrawData = params\n\n if isTokenXMLActive == true then\n broadcastToAll(\"Clear already active buttons first, then try again\", \"Red\")\n return\n end\n\n if #chaosTokens == 0 then\n broadcastToAll(\"No tokens found in play area\", \"Red\")\n return\n end\n\n -- nil handling\n redrawData.VALID_TOKENS = redrawData.VALID_TOKENS or {}\n redrawData.INVALID_TOKENS = redrawData.INVALID_TOKENS or {}\n\n -- determine if only some tokens are able to be returned to the bag\n local matchingTokensInPlay = {}\n for _, token in ipairs(chaosTokens) do\n local tokenName = getReadableTokenName(token.getName())\n\n -- allow valid tokens or not invalid tokens, also allow any token if both lists empty\n if (redrawData.VALID_TOKENS[tokenName] ~= nil and isTableEmpty(redrawData.INVALID_TOKENS)) or\n (isTableEmpty(redrawData.VALID_TOKENS) and not redrawData.INVALID_TOKENS[tokenName]) or\n (isTableEmpty(redrawData.VALID_TOKENS) and isTableEmpty(redrawData.INVALID_TOKENS)) then\n table.insert(matchingTokensInPlay, token)\n end\n end\n\n -- proceed according to number of matching tokens\n if #matchingTokensInPlay == 0 then\n broadcastToAll(\"No eligible token found in play area\", \"Red\")\n elseif #matchingTokensInPlay == 1 then\n returnAndRedraw(_, matchingTokensInPlay[1].getGUID())\n else\n -- draw XML to allow choosing the token to return to bag\n isTokenXMLActive = true\n for _, token in ipairs(matchingTokensInPlay) do\n token.UI.setXmlTable({\n {\n tag = \"VerticalLayout\",\n attributes = {\n height = 275,\n width = 275,\n padding = \"0 0 20 25\",\n scale = \"0.4 0.4 1\",\n rotation = \"0 0 180\",\n position = \"0 0 -15\",\n color = \"rgba(0,0,0,0.7)\",\n onClick = \"Global/returnAndRedraw(\" .. token.getGUID() .. \")\",\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n fontSize = \"100\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n text = \"Redraw\"\n }\n },\n {\n tag = \"Text\",\n attributes = {\n fontSize = \"125\",\n font = \"font_arkhamicons\",\n color = \"#ffffff\",\n text = \"u\"\n }\n }\n }\n }\n })\n end\n end\nend\n\n-- returns a chaos token to the chaos bag and redraws another\nfunction returnAndRedraw(_, tokenGUID)\n local returnedToken = getObjectFromGUID(tokenGUID)\n local tokenName = returnedToken.getName()\n local indexOfReturnedToken = getTokenIndex(returnedToken)\n local matColor = playermatApi.getMatColorByPosition(returnedToken.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n local takeParameters = {\n position = returnedToken.getPosition(),\n rotation = returnedToken.getRotation()\n }\n\n if #chaosTokens \u003e indexOfReturnedToken then\n takeParameters.rotation = takeParameters.rotation + Vector(0, 0, -8)\n end\n\n -- perform the actual token replacing\n trackChaosToken(tokenName, mat.getGUID(), true)\n local params = {token = returnedToken, fromBag = true}\n returnChaosTokenToBag(params)\n\n chaosTokens[indexOfReturnedToken] = drawChaosToken({\n mat = mat,\n drawAdditional = true,\n tokenType = redrawData.DRAW_SPECIFIC_TOKEN, -- currently only used for Nkosi Mabati\n takeParameters = takeParameters\n })\n\n -- remove these tokens from the bag\n if redrawData.RETURN_TO_POOL then\n -- let the bless/curse manager handle these\n if tokenName == \"Bless\" or tokenName == \"Curse\" then\n blessCurseManagerApi.removeToken(tokenName)\n else\n local invertedTable = createChaosTokenNameLookupTable()\n removeChaosToken(invertedTable[tokenName])\n end\n end\n\n -- remove XML from tokens in play\n isTokenXMLActive = false\n for _, token in ipairs(chaosTokens) do\n token.UI.setXml(\"\")\n end\n\n redrawData = {}\n\n -- return a reference to the freshly drawn token\n return chaosTokens[indexOfReturnedToken]\nend\n\n-- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n-- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n-- contents of the bag should check this method before doing so.\n-- This method will broadcast a message to all players if the bag is being searched.\n---@return boolean: True if the bag is manipulated, false if it should be blocked.\nfunction canTouchChaosTokens()\n for _, searching in pairs(bagSearchers) do\n if searching then\n broadcastToAll(\"Someone is searching the chaos bag, can't touch the tokens.\", \"Red\")\n return false\n end\n end\n return true\nend\n\n-- converts the human readable name to the empty name that the bag uses\nfunction getChaosTokenName(tokenName)\n if tokenName == \"Custom Token\" then\n tokenName = \"\"\n end\n return tokenName\nend\n\n-- converts the empty name to the human readable name\nfunction getReadableTokenName(tokenName)\n if tokenName == \"\" then\n tokenName = \"Custom Token\"\n end\n return tokenName\nend\n\n-- called by playermats (by the \"Draw chaos token\" button)\nfunction drawChaosToken(params)\n if not canTouchChaosTokens() then return end\n\n local matGUID = params.mat.getGUID()\n\n -- return token(s) on other playermat first\n if chaosTokensLastMatGUID ~= nil and chaosTokensLastMatGUID ~= matGUID and #chaosTokens ~= 0 then\n returnChaosTokens()\n chaosTokensLastMatGUID = nil\n return\n end\n\n chaosTokensLastMatGUID = matGUID\n\n -- if we have left clicked and have no tokens OR if we have right clicked\n if params.drawAdditional or #chaosTokens == 0 then\n local chaosBag = findChaosBag()\n if #chaosBag.getObjects() == 0 then return end\n chaosBag.shuffle()\n\n -- add the token to the list, compute new position based on list length\n local tokenOffset = Vector(-1.55 + 0.17 * #chaosTokens, 0.25, -0.58)\n local takeParameters = params.takeParameters or {}\n takeParameters.position = takeParameters.position or params.mat.positionToWorld(tokenOffset)\n takeParameters.rotation = takeParameters.rotation or params.mat.getRotation()\n\n local token\n if params.guidToBeResolved then\n -- resolve a sealed token from a card\n token = getObjectFromGUID(params.guidToBeResolved)\n token.setPositionSmooth(takeParameters.position)\n tokenArrangerApi.layout()\n\n local tokenName = token.getName()\n if tokenName == \"Bless\" or tokenName == \"Curse\" then\n blessCurseManagerApi.releasedToken(tokenName, token.getGUID())\n end\n else\n -- take a token from the bag, either specified or random\n if params.tokenType then\n for i, lookedForToken in ipairs(chaosBag.getObjects()) do\n if lookedForToken.nickname == params.tokenType then\n takeParameters.index = i - 1\n end\n end\n end\n token = chaosBag.takeObject(takeParameters)\n end\n\n -- get data for token description\n local name = token.getName()\n local tokenData = mythosAreaApi.returnTokenData().tokenData or {}\n local specificData = tokenData[name] or {}\n token.setDescription(specificData.description or \"\")\n trackChaosToken(name, matGUID)\n\n if not params.takeParameters then\n table.insert(chaosTokens, token)\n end\n return token\n else\n returnChaosTokens()\n end\nend\n\n---------------------------------------------------------\n-- token spawning\n---------------------------------------------------------\n\n-- DEPRECATED. Use TokenManager instead.\n-- Spawns a single token.\n---@param params table Array with arguments to the method. 1 = position, 2 = type, 3 = rotation\nfunction spawnToken(params)\n return tokenManager.spawnToken(params[1], params[2], params[3])\nend\n\n---------------------------------------------------------\n-- chaos token stat tracker\n---------------------------------------------------------\n\nfunction trackChaosToken(tokenName, matGUID, subtract)\n -- initialize tables\n if not tokenDrawingStats[matGUID] then tokenDrawingStats[matGUID] = {} end\n\n -- increase stats by 1 (or decrease if token is returned)\n local modifier = (subtract and -1 or 1)\n tokenName = getReadableTokenName(tokenName)\n tokenDrawingStats[\"Overall\"][tokenName] = (tokenDrawingStats[\"Overall\"][tokenName] or 0) + modifier\n tokenDrawingStats[matGUID][tokenName] = (tokenDrawingStats[matGUID][tokenName] or 0) + modifier\nend\n\n-- Left-click: print stats, Right-click: reset stats\nfunction handleStatTrackerClick(_, _, isRightClick)\n if isRightClick then\n resetChaosTokenStatTracker()\n else\n local squidKing = \"Nobody\"\n local maxSquid = 0\n local foundAnyStats = false\n\n for key, personalStats in pairs(tokenDrawingStats) do\n local playerColor, playerName\n\n if key == \"Overall\" then\n playerColor = \"White\"\n playerName = \"Overall\"\n else\n local matColor = playermatApi.getMatColorByPosition(getObjectFromGUID(key).getPosition())\n playerColor = playermatApi.getPlayerColor(matColor)\n playerName = Player[playerColor].steam_name or playerColor\n\n local playerSquidCount = personalStats[\"Auto-fail\"] or 0\n if playerSquidCount \u003e maxSquid then\n squidKing = playerName\n maxSquid = playerSquidCount\n end\n end\n\n -- get the total count of drawn tokens for the player\n local totalCount = 0\n for _, value in pairs(personalStats) do\n totalCount = totalCount + value\n end\n\n -- only print the personal stats if any tokens were drawn\n if totalCount \u003e 0 then\n foundAnyStats = true\n printToAll(\"------------------------------\")\n printToAll(playerName .. \" Stats\", playerColor)\n\n -- print stats in order of the \"ID_URL_MAP\"\n for _, subtable in pairs(ID_URL_MAP) do\n local tokenName = subtable.name\n local value = personalStats[tokenName]\n if value and value ~= 0 then\n printToAll(tokenName .. ': ' .. tostring(value))\n end\n end\n\n -- also print stats for custom tokens\n local customTokenName = getReadableTokenName(\"\")\n local customTokenCount = personalStats[customTokenName]\n if customTokenCount and customTokenCount ~= 0 then\n printToAll(customTokenName .. ': ' .. tostring(customTokenCount))\n end\n\n printToAll('Total: ' .. tostring(totalCount))\n end\n end\n\n -- detect if any player drew tokens\n if foundAnyStats then\n printToAll(\"------------------------------\")\n printToAll(squidKing .. \" is an auto-fail magnet.\", { 255, 0, 0 })\n else\n printToAll(\"No tokens have been drawn yet.\", \"Yellow\")\n end\n end\nend\n\n-- resets the count for each token to 0\nfunction resetChaosTokenStatTracker()\n tokenDrawingStats = { [\"Overall\"] = {} }\nend\n\n---------------------------------------------------------\n-- Difficulty selector script\n---------------------------------------------------------\n\n-- called for button creation on the difficulty selectors\n---@param args table Parameters for this function:\n-- object TTSObject Usually \"self\"\n-- key String Name of the scenario\nfunction createSetupButtons(args)\n local data = getDataValue('modeData', args.key)\n if data ~= nil then\n local buttonParameters = {}\n buttonParameters.function_owner = args.object\n buttonParameters.position = { 0, 0.1, -0.15 }\n buttonParameters.scale = { 0.47, 1, 0.47 }\n buttonParameters.height = 200\n buttonParameters.width = 1150\n buttonParameters.color = { 0.87, 0.8, 0.7 }\n\n if data.easy ~= nil then\n buttonParameters.label = \"Easy\"\n buttonParameters.click_function = \"easyClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.normal ~= nil then\n buttonParameters.label = \"Standard\"\n buttonParameters.click_function = \"normalClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.hard ~= nil then\n buttonParameters.label = \"Hard\"\n buttonParameters.click_function = \"hardClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.expert ~= nil then\n buttonParameters.label = \"Expert\"\n buttonParameters.click_function = \"expertClick\"\n args.object.createButton(buttonParameters)\n buttonParameters.position[3] = buttonParameters.position[3] + 0.20\n end\n\n if data.standalone ~= nil then\n buttonParameters.label = \"Standalone\"\n buttonParameters.click_function = \"standaloneClick\"\n args.object.createButton(buttonParameters)\n end\n end\nend\n\n-- called for adding chaos tokens\n---@param args table Parameters for this function:\n-- object object Usually \"self\"\n-- key string Name of the scenario\n-- mode string difficulty (e.g. \"hard\" or \"expert\")\nfunction fillContainer(args)\n local data = getDataValue('modeData', args.key)\n if data == nil then return end\n\n local value = data[args.mode]\n if value == nil or value.token == nil then return end\n\n local tokenList = {}\n\n for _, tokenId in ipairs(value.token) do\n table.insert(tokenList, tokenId)\n end\n\n if value.append ~= nil then\n for _, tokenId in ipairs(value.append) do\n table.insert(tokenList, tokenId)\n end\n end\n\n -- randomly choose tokens for specific Carcosa scenarios in standalone\n if value.random then\n local n = #value.random\n if n \u003e 0 then\n for _, tokenId in ipairs(value.random[math.random(1, n)]) do\n table.insert(tokenList, tokenId)\n end\n end\n end\n\n setChaosBagState(tokenList)\n\n if value.message then\n broadcastToAll(value.message)\n end\n\n if value.warning then\n broadcastToAll(value.warning, { 1, 0.5, 0.5 })\n end\nend\n\nfunction getDataValue(storage, key)\n local DATA_HELPER = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n local data = DATA_HELPER.getTable(storage)\n if data ~= nil then\n local value = data[key]\n if value ~= nil then\n local res = {}\n for m, v in pairs(value) do\n res[m] = v\n if res[m].parent ~= nil then\n local parentData = getDataValue(storage, res[m].parent)\n if parentData ~= nil and parentData[m] ~= nil and parentData[m].token ~= nil then\n res[m].token = parentData[m].token\n end\n res[m].parent = nil\n end\n end\n return res\n end\n end\nend\n\nfunction createChaosTokenNameLookupTable()\n local namesToIds = {}\n for k, v in pairs(ID_URL_MAP) do\n namesToIds[v.name] = k\n end\n return namesToIds\nend\n\n-- returns the currently drawn chaos tokens\n---@api ChaosBagApi\nfunction getChaosTokensinPlay()\n return chaosTokens\nend\n\n-- returns a table of chaos token ids in the current chaos bag\n---@api ChaosBag / ChaosBagApi\nfunction getChaosBagState()\n local tokens = {}\n local invertedTable = createChaosTokenNameLookupTable()\n local chaosBag = findChaosBag()\n\n for _, v in ipairs(chaosBag.getObjects()) do\n local id = invertedTable[v.name]\n if id then\n table.insert(tokens, id)\n else\n printToAll(v.name .. \" token not recognized. Will not be recorded.\", \"Yellow\")\n end\n end\n\n return tokens\nend\n\n-- respawns the chaos bag with a new state of tokens\n---@param tokenList table List of chaos token ids\n---@api ChaosBag / ChaosBagApi\nfunction setChaosBagState(tokenList)\n if not canTouchChaosTokens() then return end\n\n local chaosBag = findChaosBag()\n local chaosBagData = chaosBag.getData()\n local reserveData = getObjectFromGUID(\"106418\").getData()\n local tokenCache = {}\n local containedObjects = {}\n\n -- create a temporary copy of the data for each chaos token\n for _, objData in ipairs(reserveData.ContainedObjects) do\n tokenCache[objData.Nickname] = objData\n end\n\n -- iterate over tokenlist and insert specified tokens into new table\n for _, tokenId in ipairs(tokenList) do\n local tokenName = ID_URL_MAP[tokenId].name\n table.insert(containedObjects, tokenCache[tokenName])\n end\n\n -- overwrite chaos bag content and respawn it\n chaosBagData.ContainedObjects = containedObjects\n chaosBag.destruct()\n spawnObjectData({ data = chaosBagData })\n\n -- remove tokens that are still in play\n for _, token in pairs(chaosTokens) do\n if token ~= nil then token.destruct() end\n end\n chaosTokens = {}\n chaosTokensLastMatGUID = nil\n\n -- reset bless / curse manager\n blessCurseManagerApi.removeTakenTokensAndReset()\n\n printToAll(\"Chaos Bag set to chosen difficulty.\", \"Green\")\nend\n\n-- spawns the specified chaos token and puts it into the chaos bag\n---@param id string ID of the chaos token\nfunction spawnChaosToken(id)\n if not canTouchChaosTokens() then return end\n\n id = id:lower()\n local chaosBag = findChaosBag()\n local url = ID_URL_MAP[id].url or \"\"\n\n if url ~= \"\" then\n return spawnObject({\n type = 'Custom_Tile',\n position = { 0.49, 3, 0 },\n scale = { 0.81, 1.0, 0.81 },\n rotation = { 0, 270, 0 },\n callback_function = function(obj)\n obj.setName(ID_URL_MAP[id].name)\n chaosBag.putObject(obj)\n tokenArrangerApi.layout()\n end\n }).setCustomObject({\n type = 2,\n image = url,\n thickness = 0.1\n })\n end\nend\n\n-- removes the specified chaos token from the chaos bag\n---@param id string ID of the chaos token\nfunction removeChaosToken(id)\n if not canTouchChaosTokens() then return end\n\n local tokens = {}\n local chaosBag = findChaosBag()\n local name = ID_URL_MAP[id].name\n\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == name then table.insert(tokens, v.guid) end\n end\n\n -- error handling: no matching token found\n if #tokens == 0 then\n printToAll(\"No \" .. name .. \" tokens in the chaos bag.\", \"Yellow\")\n return\n end\n\n chaosBag.takeObject({\n guid = tokens[1],\n smooth = false,\n callback_function = function(obj)\n obj.destruct()\n tokenArrangerApi.layout()\n end\n })\n printToAll(\"Removing \" .. name .. \" token (in bag: \" .. #tokens - 1 .. \")\", \"White\")\nend\n\n-- returns all sealed tokens on cards to the chaos bag\nfunction releaseAllSealedTokens(playerColor)\n for _, obj in ipairs(getObjectsWithTag(\"CardThatSeals\")) do\n obj.call(\"releaseAllTokens\", playerColor)\n end\nend\n\n---------------------------------------------------------\n-- Content Importing and XML functions\n---------------------------------------------------------\n\n-- forwards the requested content type to the update function and sets highlight to clicked tab\n---@param tabId string Id of the clicked tab\nfunction onClick_tab(_, _, tabId)\n for listId, listContent in pairs(tabIdTable) do\n if listId == tabId then\n UI.setClass(listId, 'downloadTab activeTab')\n contentToShow = listContent\n else\n UI.setClass(listId, 'downloadTab')\n end\n end\n currentListItem = 1\n updateDownloadItemList()\nend\n\n-- click function for the items in the download window\n-- updates backgroundcolor for row panel and fontcolor for list item\nfunction onClick_select(_, _, identificationKey)\n UI.setAttribute(\"panel\" .. currentListItem, \"color\", \"clear\")\n UI.setAttribute(contentToShow .. \"_\" .. currentListItem, \"color\", \"white\")\n\n -- parses the identification key (contentToShow_currentListItem)\n if identificationKey then\n contentToShow = nil\n currentListItem = nil\n for str in string.gmatch(identificationKey, \"([^_]+)\") do\n if not contentToShow then\n -- grab the first part to know the content type\n contentToShow = str\n else\n -- get the index\n currentListItem = tonumber(str)\n break\n end\n end\n end\n\n UI.setAttribute(\"panel\" .. currentListItem, \"color\", \"grey\")\n UI.setAttribute(contentToShow .. \"_\" .. currentListItem, \"color\", \"black\")\n updatePreviewWindow()\nend\n\n-- click function for the \"Custom URL\" button in the playarea image gallery\nfunction onClick_customUrl(player)\n changeWindowVisibilityForColor(player.color, \"playareaGallery\")\n Wait.time(function()\n player.showInputDialog(\"Enter a custom URL for the playarea image\", \"\", function(newURL)\n playAreaApi.updateSurface(newURL)\n end)\n end, 0.15)\nend\n\n-- click function for the download button in the preview window\nfunction onClick_download(player)\n local params = library[contentToShow][currentListItem]\n params.player = player\n placeholder_download(params)\nend\n\n-- the download button on the placeholder objects calls this to directly initiate a download\n---@param params table contains url and guid of replacement object\nfunction placeholder_download(params)\n function downloadCoroutine()\n -- show progress bar\n UI.setAttribute('download_progress', 'active', true)\n\n -- update progress bar\n while requestObj do\n UI.setAttribute('download_progress', 'percentage', requestObj.download_progress * 100)\n coroutine.yield(0)\n end\n UI.setAttribute('download_progress', 'percentage', 100)\n\n -- wait 30 frames\n for i = 1, 30 do\n coroutine.yield(0)\n end\n\n -- hide progress bar\n UI.setAttribute('download_progress', 'active', false)\n\n -- hide download window\n if params.player then\n changeWindowVisibilityForColor(params.player.color, \"downloadWindow\", false)\n end\n return 1\n end\n\n local url = SOURCE_REPO .. '/' .. params.url\n requestObj = WebRequest.get(url, function(request) contentDownloadCallback(request, params) end)\n startLuaCoroutine(Global, 'downloadCoroutine')\nend\n\n-- spawns a bag that contains every object from the library\nfunction onClick_downloadAll(player)\n broadcastToAll(\"Download initiated - this will take a few minutes!\")\n\n -- hide download window\n changeWindowVisibilityForColor(player.color, \"downloadWindow\", false)\n\n startLuaCoroutine(Global, \"coroutineDownloadAll\")\nend\n\nfunction coroutineDownloadAll()\n local JSON = [[\n {\n \"Name\": \"Bag\",\n \"Transform\": {\n \"posX\": {{POSX}},\n \"posY\": 2,\n \"posZ\": -95,\n \"rotX\": 0,\n \"rotY\": 270,\n \"rotZ\": 0,\n \"scaleX\": 1,\n \"scaleY\": 1,\n \"scaleZ\": 1\n },\n \"Nickname\": \"{{NICKNAME}}\",\n \"Bag\": {\n \"Order\": 0\n },\n \"ContainedObjects\": [\n ]]\n\n local posx = -45.0\n local downloadedItems = 0\n local skippedItems = 0\n\n -- loop through the library to add content\n for contentType, objectList in pairs(library) do\n broadcastToAll(\"Downloading \" .. contentType .. \"...\")\n local contained = \"\"\n for _, params in ipairs(objectList) do\n local request = WebRequest.get(SOURCE_REPO .. '/' .. params.url, function() end)\n local start = os.time()\n while true do\n if request.is_done then\n contained = contained .. request.text .. \",\"\n downloadedItems = downloadedItems + 1\n break\n -- time-out if item can't be loaded in 5s\n elseif request.is_error or (os.time() - start) \u003e 5 then\n skippedItems = skippedItems + 1\n break\n end\n coroutine.yield(0)\n end\n end\n local JSONCopy = JSON\n JSONCopy = JSONCopy .. contained .. \"]}\"\n JSONCopy = JSONCopy:gsub(\"{{POSX}}\", posx)\n JSONCopy = JSONCopy:gsub(\"{{NICKNAME}}\", contentType)\n spawnObjectJSON({ json = JSONCopy })\n posx = posx + 3\n end\n\n broadcastToAll(downloadedItems .. \" objects downloaded.\", \"Green\")\n broadcastToAll(skippedItems .. \" objects had a time-out / error.\", \"Orange\")\n return 1\nend\n\n-- spawns a placeholder box for the selected object\nfunction onClick_spawnPlaceholder(player)\n -- get object references\n local item = library[contentToShow][currentListItem]\n local dummy = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlaceholderBoxDummy\")\n\n -- error handling\n if not item.boxsize or item.boxsize == \"\" or not item.boxart or item.boxart == \"\" then\n print(\"Error loading object.\")\n return\n end\n\n -- get data for placeholder\n local spawnPos = { -39.5, 2, -87 }\n\n local meshTable = {\n big = \"https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_MSH.obj\",\n small = \"https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/tuckbox_h_MSH.obj\",\n wide = \"http://cloud-3.steamusercontent.com/ugc/2278324073260846176/33EFCAF30567F8756F665BE5A2A6502E9C61C7F7/\"\n }\n\n local scaleTable = {\n big = { 1.00, 0.14, 1.00 },\n small = { 2.21, 0.46, 2.42 },\n wide = { 2.00, 0.11, 1.69 }\n }\n\n local placeholder = spawnObject({\n type = \"Custom_Model\",\n position = spawnPos,\n rotation = { 0, 270, 0 },\n scale = scaleTable[item.boxsize],\n })\n\n placeholder.setCustomObject({\n mesh = meshTable[item.boxsize],\n diffuse = item.boxart,\n material = 3\n })\n\n if item.boxsize == \"big\" then\n placeholder.addTag(\"LargeBox\")\n end\n\n placeholder.setColorTint({ 1, 1, 1, 71 / 255 })\n placeholder.setName(item.name)\n placeholder.setDescription(\"by \" .. (item.author or \"Unknown\"))\n placeholder.setGMNotes(item.url)\n placeholder.setLuaScript(dummy.getLuaScript())\n Player.getPlayers()[1].pingTable(spawnPos)\n\n -- hide download window\n changeWindowVisibilityForColor(player.color, \"downloadWindow\", false)\nend\n\n-- toggles the visibility of the respective UI\n---@param player tts__Player Player that triggered this\n---@param windowId string Name of the UI to toggle\nfunction onClick_toggleUi(player, windowId)\n if windowId == \"Navigation Overlay\" then\n navigationOverlayApi.cycleVisibility(player.color)\n return\n end\n\n -- hide the playAreaGallery if visible\n if windowId == \"downloadWindow\" then\n changeWindowVisibilityForColor(player.color, \"playAreaGallery\", false)\n -- hide the downloadWindow if visible\n elseif windowId == \"playAreaGallery\" then\n changeWindowVisibilityForColor(player.color, \"downloadWindow\", false)\n end\n\n changeWindowVisibilityForColor(player.color, windowId)\nend\n\n-- toggles the visibility of the specific window for the specified color\n---@param color string Player color to toggle the visibility for\n---@param windowId string ID of the XML element\n---@param overrideState? boolean Forcefully sets the new visibility\n---@return boolean visible Returns the new state of the visibility\nfunction changeWindowVisibilityForColor(color, windowId, overrideState)\n -- current state\n local colorString = UI.getAttribute(windowId, \"visibility\") or \"\"\n\n -- parse the visibility string\n local visible = false\n local viewers = {}\n for str in string.gmatch(colorString, \"%a+\") do\n table.insert(viewers, str)\n if str == color then\n visible = true\n end\n end\n\n -- add / remove the color as viewer\n if visible == true then\n removeValueFromTable(viewers, color)\n elseif visible == false then\n table.insert(viewers, color)\n end\n visible = not visible\n\n -- resolve override\n if overrideState == true and visible == false then\n table.insert(viewers, color)\n visible = true\n elseif overrideState == false and visible == true then\n removeValueFromTable(viewers, color)\n visible = false\n end\n\n -- construct new string\n local newColorString = \"\"\n for _, viewer in ipairs(viewers) do\n newColorString = newColorString .. viewer .. \"|\"\n end\n\n -- remove last delimiter\n newColorString = newColorString:sub(1, -2)\n\n -- update the visibility of the XML\n UI.setAttribute(windowId, \"visibility\", newColorString)\n UI.setAttribute(windowId, \"active\", newColorString ~= \"\")\n\n return visible\nend\n\n-- forwards the call to the onClick function\nfunction togglePlayAreaGallery(playerColor)\n changeWindowVisibilityForColor(playerColor, \"playareaGallery\")\nend\n\n-- updates the preview window\nfunction updatePreviewWindow()\n local item = library[contentToShow][currentListItem]\n local tempImage =\n \"http://cloud-3.steamusercontent.com/ugc/2115061845788345842/2CD6ABC551555CCF58F9D0DDB7620197BA398B06/\"\n\n -- set default image if not defined\n if item.boxsize == nil or item.boxsize == \"\" or item.boxart == nil or item.boxart == \"\" then\n item.boxsize = \"big\"\n item.boxart = \"http://cloud-3.steamusercontent.com/ugc/762723517667628371/18438B0A0045038A7099648AA3346DFCAA267C66/\"\n end\n\n UI.setValue(\"previewTitle\", item.name)\n UI.setValue(\"previewAuthor\", \"by \" .. (item.author or \"- Author not found -\"))\n UI.setValue(\"previewDescription\", item.description or \"- Description not found -\")\n\n -- update mask according to size (hardcoded values to align image in mask)\n local maskData = {}\n if item.boxsize == \"big\" then\n maskData = {\n image = \"box-cover-mask-big\",\n width = \"870\",\n height = \"435\",\n offsetXY = \"154 60\"\n }\n elseif item.boxsize == \"small\" then\n maskData = {\n image = \"box-cover-mask-small\",\n width = \"792\",\n height = \"594\",\n offsetXY = \"135 13\"\n }\n elseif item.boxsize == \"wide\" then\n maskData = {\n image = \"box-cover-mask-wide\",\n width = \"756\",\n height = \"630\",\n offsetXY = \"-190 -70\"\n }\n end\n\n -- loading empty image as placeholder until real image is loaded\n UI.setAttribute(\"previewArtImage\", \"image\", tempImage)\n\n -- insert the image itself\n UI.setAttribute(\"previewArtImage\", \"image\", item.boxart)\n UI.setAttributes(\"previewArtMask\", maskData)\nend\n\n-- formats the json response from the webrequest into a key-value lua table\n-- strips the prefix from the community content items\nfunction formatLibrary(json_response)\n library = {}\n library[\"campaigns\"] = json_response.campaigns\n library[\"scenarios\"] = json_response.scenarios\n library[\"extras\"] = json_response.extras\n library[\"fanmadeCampaigns\"] = {}\n library[\"fanmadeScenarios\"] = {}\n library[\"fanmadePlayerCards\"] = {}\n\n for _, item in ipairs(json_response.community) do\n local identifier = nil\n for str in string.gmatch(item.name, \"([^:]+)\") do\n if not identifier then\n -- grab the first part to know the content type\n identifier = str\n else\n -- update the name without the content type\n item.name = str\n break\n end\n end\n\n if identifier == \"Fan Investigators\" then\n table.insert(library[\"fanmadePlayerCards\"], item)\n elseif identifier == \"Fan Campaign\" then\n table.insert(library[\"fanmadeCampaigns\"], item)\n elseif identifier == \"Fan Scenario\" then\n table.insert(library[\"fanmadeScenarios\"], item)\n end\n end\nend\n\n-- updates the window content to the requested content\nfunction updateDownloadItemList()\n if not library then return end\n\n -- addition of list items according to library file\n local globalXml = UI.getXmlTable()\n local contentList = getXmlTableElementById(globalXml, 'contentList')\n\n contentList.children = {}\n for i, v in ipairs(library[contentToShow]) do\n table.insert(contentList.children,\n {\n tag = \"Panel\",\n attributes = { id = \"panel\" .. i },\n children = {\n tag = 'Text',\n value = v.name,\n attributes = {\n id = contentToShow .. \"_\" .. i,\n onClick = 'onClick_select',\n alignment = 'MiddleLeft'\n }\n }\n })\n end\n\n contentList.attributes.height = #contentList.children * 27\n updateGlobalXml(globalXml)\n\n -- select the first item\n Wait.time(onClick_select, 0.2)\nend\n\n-- this helper function updates the global XML while preserving the visibility of windows\nfunction updateGlobalXml(newXml)\n -- preserve visibility settings for these elements\n local windowIdList = {\n \"playAreaGallery\",\n \"downloadWindow\",\n \"optionPanel\"\n }\n\n -- get current state and update newXml\n for _, windowId in ipairs(windowIdList) do\n local element = getXmlTableElementById(newXml, windowId)\n element.attributes.active = UI.getAttribute(windowId, \"active\")\n element.attributes.visibility = UI.getAttribute(windowId, \"visibility\")\n end\n\n UI.setXmlTable(newXml)\nend\n\n-- called after the webrequest of downloading an item\n-- deletes the placeholder and spawns the downloaded item\nfunction contentDownloadCallback(request, params)\n requestObj = nil\n\n -- error handling\n if request.is_error or request.response_code ~= 200 then\n print('Error: ' .. request.error)\n return\n end\n\n -- initiate content spawning\n local spawnTable = { json = request.text }\n if params.replace then\n local replacedObject = getObjectFromGUID(params.replace)\n if replacedObject then\n spawnTable.position = replacedObject.getPosition()\n spawnTable.rotation = replacedObject.getRotation()\n spawnTable.scale = replacedObject.getScale()\n destroyObject(replacedObject)\n end\n end\n\n -- if position is undefined, get empty position\n if not spawnTable.position then\n spawnTable.rotation = { 0, 270, 0 }\n\n local pos = getValidSpawnPosition()\n if pos then\n spawnTable.position = pos\n else\n broadcastToAll(\n \"Please make space in the area below the tentacle stand in the upper middle of the table and try again.\", \"Red\")\n return\n end\n end\n\n -- if spawned from menu, move the camera and/or ping the table\n if params.name then\n spawnTable[\"callback_function\"] = function(obj)\n Wait.time(function()\n -- move camera\n if params.player then\n params.player.lookAt({\n position = obj.getPosition(),\n pitch = 65,\n yaw = 90,\n distance = 65\n })\n end\n\n -- ping object\n local pingPlayer = params.player or Player.getPlayers()[1]\n pingPlayer.pingTable(obj.getPosition())\n end, 0.1)\n end\n end\n\n if pcall(function() spawnObjectJSON(spawnTable) end) then\n print('Object loaded.')\n else\n print('Error loading object.')\n end\nend\n\n-- gets the first empty position to spawn a custom content object safely\nfunction getValidSpawnPosition()\n local potentialSpawnPositionX = { 65, 50, 35 }\n local potentialSpawnPositionY = 1.5\n local potentialSpawnPositionZ = { 35, 21, 7, -7, -21, -35 }\n\n for i, posX in ipairs(potentialSpawnPositionX) do\n for j, posZ in ipairs(potentialSpawnPositionZ) do\n local pos = {\n x = posX,\n y = potentialSpawnPositionY,\n z = posZ,\n }\n if checkPositionForContentSpawn(pos) then\n return pos\n end\n end\n end\n return nil\nend\n\n-- checks whether something is in the specified position\n-- returns true if empty\nfunction checkPositionForContentSpawn(checkPos)\n local searchResult = searchLib.atPosition(checkPos)\n\n -- first hit is the table surface, additional hits means something is there\n return #searchResult == 1\nend\n\n-- downloading of the library file\nfunction libraryDownloadCallback(request)\n if request.is_error or request.response_code ~= 200 then\n print('error: ' .. request.error)\n return\n end\n\n local json_response = nil\n if pcall(function() json_response = JSON.decode(request.text) end) then\n formatLibrary(json_response)\n updateDownloadItemList()\n else\n print('error parsing downloaded library')\n end\nend\n\n-- loops through an XML table and returns the specified object\n---@param ui table XmlTable (get this via getXmlTable)\n---@param id string Id of the object to return\nfunction getXmlTableElementById(ui, id)\n for _, obj in ipairs(ui) do\n if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end\n if obj.children then\n local result = getXmlTableElementById(obj.children, id)\n if result then return result end\n end\n end\n return nil\nend\n\n---------------------------------------------------------\n-- Option Panel related functionality\n---------------------------------------------------------\n\n-- changes the UI state and the internal variable for the togglebuttons\nfunction onClick_toggleOption(_, _, id)\n local currentState = optionPanel[id]\n local newState = not currentState\n applyOptionPanelChange(id, newState)\n UI.setAttribute(id, \"image\", newState and \"option_on\" or \"option_off\")\nend\n\n-- color selection for playArea\nfunction onClick_playAreaConnectionColor(player, _, id)\n player.showColorDialog(optionPanel[id], function(color)\n applyOptionPanelChange(id, color)\n end)\nend\n\n-- called by the language selection dropdown\nfunction languageSelected(_, selectedIndex, id)\n optionPanel[id] = LANGUAGES[tonumber(selectedIndex) + 1].code\nend\n\n-- returns the ID (position in the table) for a provided language code\nfunction returnLanguageId(code)\n for index, tbl in ipairs(LANGUAGES) do\n if tbl.code == code then\n return index\n end\n end\nend\n\n-- called by the resource counter selection dropdown\nfunction resourceCounterSelected(_, selectedIndex, id)\n optionPanel[id] = RESOURCE_OPTIONS[tonumber(selectedIndex) + 1]\nend\n\n-- returns the ID for the provided option name\nfunction returnResourceCounterId(name)\n for index, optionName in ipairs(RESOURCE_OPTIONS) do\n if optionName == name then\n return index\n end\n end\nend\n\n-- called by the playermat removal selection dropdown\nfunction playermatRemovalSelected(player, selectedIndex, id)\n if selectedIndex == \"0\" then return end\n\n local matColorList = { \"White\", \"Orange\", \"Green\", \"Red\" }\n local matColor = matColorList[tonumber(selectedIndex)]\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n if mat then\n -- confirmation dialog about deletion\n player.pingTable(mat.getPosition())\n player.showConfirmDialog(\n \"Do you really want to remove \" .. matColor .. \"'s playermat and related objects? This can't be reversed.\",\n function()\n removePlayermat(matColor)\n end)\n else\n -- info dialog that it is already deleted\n player.showInfoDialog(matColor .. \"'s playermat has already been removed.\")\n end\n\n -- set selected value back to first option\n UI.setAttribute(id, \"value\", 0)\nend\n\n-- removes a playermat and all related objects from play\n---@param matColor string Color of the playermat to remove\nfunction removePlayermat(matColor)\n local matObjects = guidReferenceApi.getObjectsByOwner(matColor)\n if not matObjects.Playermat then return end\n\n -- remove action tokens\n local actionTokens = playermatApi.searchAroundPlayermat(matColor, \"isUniversalToken\")\n for _, obj in ipairs(actionTokens) do\n obj.destruct()\n end\n\n -- remove mat owned objects\n for _, obj in pairs(matObjects) do\n obj.destruct()\n end\nend\n\n-- sets the option panel to the correct state (corresponding to 'optionPanel')\nfunction updateOptionPanelState()\n for id, optionValue in pairs(optionPanel) do\n if id == \"cardLanguage\" and type(optionValue) == \"string\" then\n local dropdownId = returnLanguageId(optionValue) - 1\n UI.setAttribute(id, \"value\", dropdownId)\n elseif id == \"useResourceCounters\" and type(optionValue) == \"string\" then\n local dropdownId = returnResourceCounterId(optionValue) - 1\n UI.setAttribute(id, \"value\", dropdownId)\n elseif id == \"playAreaConnectionColor\" then\n UI.setAttribute(id, \"color\", \"#\" .. Color.new(optionValue):toHex())\n elseif (type(optionValue) == \"boolean\" and optionValue)\n or (type(optionValue) == \"string\" and optionValue)\n or (type(optionValue) == \"table\" and #optionValue ~= 0) then\n UI.setAttribute(id, \"image\", \"option_on\")\n else\n UI.setAttribute(id, \"image\", \"option_off\")\n end\n end\nend\n\n-- handles the applying of option selections and calls the respective functions based on the id\n---@param id string ID of the option that was selected or deselected\n---@param state boolean|any State of the option (true = enabled)\nfunction applyOptionPanelChange(id, state)\n optionPanel[id] = state\n\n -- option: Snap tags\n if id == \"useSnapTags\" then\n playermatApi.setLimitSnapsByType(state, \"All\")\n\n -- option: Draw 1 button\n elseif id == \"showDrawButton\" then\n playermatApi.showDrawButton(state, \"All\")\n\n -- option: Use class texture\n elseif id == \"useClassTexture\" then\n playermatApi.useClassTexture(state, \"All\")\n\n -- option: Clickable clue counters\n elseif id == \"useClueClickers\" then\n playermatApi.clickableClues(state, \"All\")\n\n -- update master clue counter\n local counter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MasterClueCounter\")\n counter.setVar(\"useClickableCounters\", state)\n\n -- option: Enable card helpers\n elseif id == \"enableCardHelpers\" then\n toggleCardHelpers(state)\n\n -- option: Play area connection drawing\n elseif id == \"playAreaConnections\" then\n playAreaApi.setConnectionDrawState(state)\n\n -- option: Play area connection color\n elseif id == \"playAreaConnectionColor\" then\n playAreaApi.setConnectionColor(state)\n UI.setAttribute(id, \"color\", \"#\" .. Color.new(state):toHex())\n\n -- option: Play area snap tags\n elseif id == \"playAreaSnapTags\" then\n playAreaApi.setLimitSnapsByType(state)\n\n -- option: Show clean up helper\n elseif id == \"showCleanUpHelper\" then\n spawnOrRemoveHelper(state, \"Clean Up Helper\", { -66, 1.53, 46 })\n\n -- option: Show hand helper for each player\n elseif id == \"showHandHelper\" then\n spawnOrRemoveHelperForPlayermats(\"Hand Helper\", state)\n\n -- option: Show search assistant for each player\n elseif id == \"showSearchAssistant\" then\n spawnOrRemoveHelperForPlayermats(\"Search Assistant\", state)\n\n -- option: Show attachment helper\n elseif id == \"showAttachmentHelper\" then\n spawnOrRemoveHelper(state, \"Attachment Helper\", { -62, 1.4, 0 })\n\n -- option: Show CYOA campaign guides\n elseif id == \"showCYOA\" then\n spawnOrRemoveHelper(state, \"CYOA Campaign Guides\", { 39, 1.3, -20 })\n\n -- option: Show displacement tool\n elseif id == \"showDisplacementTool\" then\n spawnOrRemoveHelper(state, \"Displacement Tool\", { -57, 1.53, 46 })\n end\nend\n\n-- spawns or removes a helper object for all playermats\n---@param helperName string Name of the helper object\n---@param state boolean Contains the state of the option: true = spawn it, false = remove it\nfunction spawnOrRemoveHelperForPlayermats(helperName, state)\n for color, data in pairs(playermatApi.getHelperSpawnData(\"All\", helperName)) do\n spawnOrRemoveHelper(state, helperName, data.position, data.rotation, color)\n end\nend\n\n-- handler for spawn / remove functions of helper objects\n---@param state boolean Contains the state of the option: true = spawn it, false = remove it\n---@param name string Name of the helper object\n---@param position tts__Vector Position of the object (where it will spawn)\n---@param rotation? tts__Vector Rotation of the object for spawning (default: {0, 270, 0})\n---@param owner? string Owner of the object (defaults to \"Mythos\")\n---@return string|nil GUID GUID of the spawnedObj (or nil if object was removed)\nfunction spawnOrRemoveHelper(state, name, position, rotation, owner)\n if state then\n Player.getPlayers()[1].pingTable(position)\n local spawnedGUID = spawnHelperObject(name, position, rotation).getGUID()\n local cleanName = name:gsub(\"%s+\", \"\")\n guidReferenceApi.editIndex(owner or \"Mythos\", cleanName, spawnedGUID)\n else\n removeHelperObject(name)\n end\nend\n\n-- copies the specified tool (by name) from the option panel source bag\n---@param name string Name of the object that should be copied\n---@param position tts__Vector Desired position of the object\n---@param rotation? tts__Vector Desired rotation of the object (defaults to object's rotation)\nfunction spawnHelperObject(name, position, rotation)\n local sourceBag = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"OptionPanelSource\")\n\n -- error handling for missing sourceBag\n if not sourceBag then\n broadcastToAll(\"Option panel source bag could not be found!\", \"Red\")\n return\n end\n\n local spawnTable = { position = position }\n\n -- only overrride rotation if there is one provided (object's rotation used instead)\n if rotation then\n spawnTable.rotation = rotation\n end\n\n for _, objData in ipairs(sourceBag.getData().ContainedObjects) do\n if objData[\"Nickname\"] == name then\n objData[\"Locked\"] = true\n spawnTable.data = objData\n return spawnObjectData(spawnTable)\n end\n end\nend\n\n-- removes the specified tool (by name)\n---@param name string Object that should be removed\nfunction removeHelperObject(name)\n local cleanName = name:gsub(\"%s+\", \"\")\n for _, obj in pairs(guidReferenceApi.getObjectsByType(cleanName)) do\n obj.destruct()\n end\nend\n\n-- loads saved options\n---@param newOptions table Contains the new state for the option panel\nfunction loadSettings(newOptions)\n for id, state in pairs(newOptions) do\n if optionPanel[id] ~= state then\n optionPanel[id] = state\n applyOptionPanelChange(id, state)\n end\n end\n\n -- update XML UI state\n updateOptionPanelState()\nend\n\n-- loads the default options\nfunction onClick_defaultSettings()\n -- clean reset of variables\n optionPanel = {\n cardLanguage = \"en\",\n changePlayAreaImage = false,\n enableCardHelpers = true,\n playAreaConnectionColor = { a = 1, b = 0.4, g = 0.4, r = 0.4 },\n playAreaConnections = true,\n playAreaSnapTags = true,\n showAttachmentHelper = false,\n showCleanUpHelper = false,\n showCYOA = false,\n showDisplacementTool = false,\n showDrawButton = false,\n showHandHelper = false,\n showSearchAssistant = false,\n showTitleSplash = true,\n useClassTexture = true,\n useClueClickers = false,\n useResourceCounters = \"disabled\",\n useSnapTags = true\n }\n\n -- applying changes\n for id, state in pairs(optionPanel) do\n applyOptionPanelChange(id, state)\n end\n\n -- update UI\n updateOptionPanelState()\nend\n\n-- splash scenario title on setup\nfunction titleSplash(scenarioName)\n if optionPanel['showTitleSplash'] then\n -- if there's any ongoing title being displayed, hide it and cancel the waiting function\n if hideTitleSplashWaitFunctionId then\n Wait.stop(hideTitleSplashWaitFunctionId)\n hideTitleSplashWaitFunctionId = nil\n UI.setAttribute('title_splash', 'active', false)\n end\n\n -- display scenario name and set a 4 seconds (2 seconds animation and 2 seconds on screen)\n -- wait timer to hide the scenario name\n UI.setValue('title_splash_text', scenarioName)\n UI.show('title_splash')\n hideTitleSplashWaitFunctionId = Wait.time(function()\n UI.hide('title_splash')\n hideTitleSplashWaitFunctionId = nil\n end, 4)\n\n soundCubeApi.playSoundByName(\"Deep Bell\")\n end\nend\n\n-- instructs all card helpers to update their visibility\nfunction toggleCardHelpers(state)\n for _, obj in ipairs(getObjectsWithTag(\"CardWithHelper\")) do\n obj.call(\"setHelperState\", state)\n end\nend\n\n---------------------------------------------------------\n-- Update notification related functionality\n---------------------------------------------------------\n\n-- grabs the latest mod version and release notes from GitHub (called onLoad())\nfunction getModVersion()\n WebRequest.get(SOURCE_REPO .. '/modversion.json', compareVersion)\nend\n\n-- compares the modversion with GitHub and possibly shows the update notification\nfunction compareVersion(request)\n if request.is_error then\n log(request.error)\n return\n end\n\n -- global variable to make it accessible for other functions\n modMeta = JSON.decode(request.text)\n\n -- stop here if on latest or newer version\n if convertVersionToNumber(MOD_VERSION) \u003e= convertVersionToNumber(modMeta[\"latestVersion\"]) then return end\n\n -- stop here if \"don't show again\" was clicked for this version before\n if acknowledgedUpgradeVersions[modMeta[\"latestVersion\"]] then return end\n\n updateNotificationLoading()\n\n -- delay to avoid lagging during onLoad()\n Wait.time(function() UI.show(\"FinnIcon\") end, 1)\nend\n\n-- converts a version number to a string\n---@param version string Version number, separated by dots (e.g. 3.3.1)\nfunction convertVersionToNumber(version)\n local major, minor, patch = string.match(version, \"(%d+)%.(%d+)%.(%d+)\")\n return major * 100 + minor * 10 + patch\nend\n\n-- updates the XML update notification based on the mod metadata\nfunction updateNotificationLoading()\n -- grab data\n local highlights = modMeta[\"releaseHighlights\"]\n\n -- concatenate the release highlights\n local highlightText = \"• \" .. highlights[1]\n for i, entry in pairs(highlights) do\n if i ~= 1 then\n highlightText = highlightText .. \"\\n• \" .. entry\n end\n end\n\n -- update the XML UI\n UI.setValue(\"notificationHeader\", \"New version available: \" .. modMeta[\"latestVersion\"])\n UI.setValue(\"releaseHighlightText\", highlightText)\n UI.setAttribute(\"highlightRow\", \"preferredHeight\", 20 * #highlights)\n UI.setAttribute(\"updateNotification\", \"height\", 20 * #highlights + 125)\nend\n\n-- close / don't show again buttons on the update notification\nfunction onClick_notification(_, parameter)\n if parameter == \"dontShowAgain\" then\n -- this variable tracks if \"don't show again\" was pressed for a version\n acknowledgedUpgradeVersions[modMeta[\"latestVersion\"]] = true\n end\n UI.hide(\"FinnIcon\")\n UI.hide(\"updateNotification\")\nend\n\n---------------------------------------------------------\n-- Utility functions\n---------------------------------------------------------\n\n-- removes a value from a table\nfunction removeValueFromTable(t, val)\n for i, v in ipairs(t) do\n if v == val then\n table.remove(t, i)\n break\n end\n end\nend\n\n-- checks if a table is empty\nfunction isTableEmpty(tbl)\n if next(tbl) == nil then\n return true\n else\n return false\n end\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/Global\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/SoundCubeApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SoundCubeApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- this table links the name of a trigger effect to its index\n local soundIndices = {\n [\"Vacuum\"] = 0,\n [\"Deep Bell\"] = 1,\n [\"Dark Souls\"] = 2\n }\n\n ---@param index number Index of the sound effect to play\n local function playTriggerEffect(index)\n local SoundCube = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"SoundCube\")\n SoundCube.AssetBundle.playTriggerEffect(index)\n end\n\n -- plays the by name requested sound\n ---@param soundName string Name of the sound to play\n SoundCubeApi.playSoundByName = function(soundName)\n playTriggerEffect(soundIndices[soundName])\n end\n\n return SoundCubeApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"acknowledgedUpgradeVersions\":[],\"chaosTokensGUID\":[],\"optionPanel\":{\"cardLanguage\":\"en\",\"changePlayAreaImage\":false,\"enableCardHelpers\":true,\"playAreaConnectionColor\":{\"a\":1,\"b\":0.4,\"g\":0.4,\"r\":0.4},\"playAreaConnections\":true,\"playAreaSnapTags\":true,\"showAttachmentHelper\":false,\"showCleanUpHelper\":false,\"showCYOA\":false,\"showDisplacementTool\":false,\"showDrawButton\":false,\"showHandHelper\":false,\"showSearchAssistant\":false,\"showTitleSplash\":true,\"useClassTexture\":true,\"useClueClickers\":false,\"useResourceCounters\":\"disabled\",\"useSnapTags\":true}}", "MusicPlayer": { "AudioLibrary": [ { @@ -590,7 +585,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GUIDReferenceHandler\")\nend)\n__bundle_register(\"core/GUIDReferenceHandler\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal GuidReferences = {\n White = {\n ClueCounter = \"d86b7c\",\n ClickableClueCounter = \"db85d6\",\n DamageCounter = \"eb08d6\",\n HandZone = \"a70eee\",\n HorrorCounter = \"468e88\",\n InvestigatorSkillTracker = \"e598c2\",\n Playermat = \"8b081b\",\n ResourceCounter = \"4406f0\",\n TokenDiscardZone = \"457de3\",\n Trash = \"147e80\"\n },\n Orange = {\n ClueCounter = \"1769ed\",\n ClickableClueCounter = \"3f22e5\",\n DamageCounter = \"e64eec\",\n HandZone = \"5fe087\",\n HorrorCounter = \"0257d9\",\n InvestigatorSkillTracker = \"b4a5f7\",\n Playermat = \"bd0ff4\",\n ResourceCounter = \"816d84\",\n TokenDiscardZone = \"457de4\",\n Trash = \"f7b6c8\"\n },\n Green = {\n ClueCounter = \"032300\",\n ClickableClueCounter = \"891403\",\n DamageCounter = \"1f5a0a\",\n HandZone = \"0285cc\",\n HorrorCounter = \"7b5729\",\n InvestigatorSkillTracker = \"af7ed7\",\n PoolResources = \"0168ae\",\n PoolDamage = \"b0ef6c\",\n PoolHorror = \"ae1a4e\",\n PoolClues = \"fae2f6\",\n PoolDoom = \"16724b\",\n Playermat = \"383d8b\",\n ResourceCounter = \"cd15ac\",\n TokenDiscardZone = \"457de5\",\n TokenRemover = \"2ba7a5\",\n Trash = \"5f896a\"\n },\n Red = {\n ClueCounter = \"37be78\",\n ClickableClueCounter = \"4111de\",\n DamageCounter = \"591a45\",\n HandZone = \"be2f17\",\n HorrorCounter = \"beb964\",\n InvestigatorSkillTracker = \"e74881\",\n PoolResources = \"fd617a\",\n PoolDamage = \"93f4a0\",\n PoolHorror = \"7bd2a0\",\n PoolClues = \"3b2550\",\n PoolDoom = \"16fcd6\",\n Playermat = \"0840d5\",\n ResourceCounter = \"a4b60d\",\n TokenDiscardZone = \"457de6\",\n TokenRemover = \"39b175\",\n Trash = \"4b8594\"\n },\n Mythos = {\n AdditionalPlayerCardsBag = \"2cba6b\",\n AllCardsBag = \"15bb07\",\n BlessCurseManager = \"5933fb\",\n CampaignThePathToCarcosa = \"aca04c\",\n ChaosBagZone = \"83ef06\",\n DataHelper = \"708279\",\n DeckImporter = \"a28140\",\n DoomCounter = \"85c4c6\",\n DoomInPlayCounter = \"652ff3\",\n InvestigatorCounter = \"f182ee\",\n MasterClueCounter = \"4a3aa4\",\n MythosArea = \"9f334f\",\n NavigationOverlayHandler = \"797ede\",\n OptionPanelSource = \"830bd0\",\n PlaceholderBoxDummy = \"a93466\",\n PlayArea = \"721ba2\",\n PlayAreaImageSelector = \"b7b45b\",\n PlayAreaZone = \"a2f932\",\n PlayerCardPanel = \"2d30ee\",\n ResourceTokenBag = \"9fadf9\",\n RulesReference = \"d99993\",\n SoundCube = \"3c988f\",\n TokenArranger = \"022907\",\n TokenSource = \"124381\",\n TokenSpawnTracker = \"e3ffc9\",\n TourStarter = \"0e5aa8\",\n Trash = \"70b9f6\",\n VictoryDisplay = \"6ccd6d\"\n }\n}\n\nlocal editsToIndex = {\n White = {},\n Orange = {},\n Green = {},\n Red = {},\n Mythos = {}\n}\n\n-- save function to keep edits to the index\nfunction onSave() return JSON.encode({ editsToIndex = editsToIndex }) end\n\n-- load function to restore edits to the index\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n editsToIndex = loadedData.editsToIndex\n updateMainIndex()\n end\nend\n\n-- merges the main index and the edits\nfunction updateMainIndex()\n for owner, subTable in pairs(editsToIndex) do\n for type, guid in pairs(subTable) do\n GuidReferences[owner][type] = guid\n end\n end\nend\n\n-- returns an object reference for the requested owner and type\nfunction getObjectByOwnerAndType(params)\n local owner = params.owner or \"Mythos\"\n local type = params.type\n return getObjectFromGUID(GuidReferences[owner][type])\nend\n\n-- returns a list of objects for the requested type\nfunction getObjectsByType(type)\n local objList = {}\n for owner, objects in pairs(GuidReferences) do\n local obj = getObjectFromGUID(objects[type])\n if obj then\n objList[owner] = obj\n end\n end\n return objList\nend\n\n-- returns a list of objects for the requested owner\nfunction getObjectsByOwner(owner)\n local objList = {}\n for type, guid in pairs(GuidReferences[owner]) do\n local obj = getObjectFromGUID(guid)\n if obj then\n objList[type] = obj\n end\n end\n return objList\nend\n\n-- makes an edit to the main index\nfunction editIndex(params)\n editsToIndex[params.owner][params.type] = params.guid\n updateMainIndex()\nend\n\n-- Returns the owner of the provided object (either the matColor or \"Mythos\")\n---@param object tts__GameObject Object to check\nfunction getOwnerOfObject(object)\n if object == nil then return end\n\n -- use GUID to check owners instead of obtaining each as reference\n local objectGuid = object.getGUID()\n\n -- check if object is directly owned\n for owner, subtable in pairs(GuidReferences) do\n for type, guid in pairs(subtable) do\n if guid == objectGuid then\n return owner\n end\n end\n end\n\n -- check if it is on an owned object\n local result = searchLib.belowPosition(object.getPosition())\n\n for owner, subtable in pairs(GuidReferences) do\n for type, guid in pairs(subtable) do\n for _, searchObj in ipairs(result) do\n if guid == searchObj.getGUID() then\n return owner\n end\n end\n end\n end\n\n -- default to \"Mythos\"\n return \"Mythos\"\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GUIDReferenceHandler\")\nend)\n__bundle_register(\"core/GUIDReferenceHandler\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal GuidReferences = {\n White = {\n ClueCounter = \"d86b7c\",\n ClickableClueCounter = \"db85d6\",\n DamageCounter = \"eb08d6\",\n HandZone = \"a70eee\",\n HorrorCounter = \"468e88\",\n InvestigatorSkillTracker = \"e598c2\",\n Playermat = \"8b081b\",\n ResourceCounter = \"4406f0\",\n TokenDiscardZone = \"457de3\",\n Trash = \"147e80\"\n },\n Orange = {\n ClueCounter = \"1769ed\",\n ClickableClueCounter = \"3f22e5\",\n DamageCounter = \"e64eec\",\n HandZone = \"5fe087\",\n HorrorCounter = \"0257d9\",\n InvestigatorSkillTracker = \"b4a5f7\",\n Playermat = \"bd0ff4\",\n ResourceCounter = \"816d84\",\n TokenDiscardZone = \"457de4\",\n Trash = \"f7b6c8\"\n },\n Green = {\n ClueCounter = \"032300\",\n ClickableClueCounter = \"891403\",\n DamageCounter = \"1f5a0a\",\n HandZone = \"0285cc\",\n HorrorCounter = \"7b5729\",\n InvestigatorSkillTracker = \"af7ed7\",\n PoolResources = \"0168ae\",\n PoolDamage = \"b0ef6c\",\n PoolHorror = \"ae1a4e\",\n PoolClues = \"fae2f6\",\n PoolDoom = \"16724b\",\n Playermat = \"383d8b\",\n ResourceCounter = \"cd15ac\",\n TokenDiscardZone = \"457de5\",\n TokenRemover = \"2ba7a5\",\n Trash = \"5f896a\"\n },\n Red = {\n ClueCounter = \"37be78\",\n ClickableClueCounter = \"4111de\",\n DamageCounter = \"591a45\",\n HandZone = \"be2f17\",\n HorrorCounter = \"beb964\",\n InvestigatorSkillTracker = \"e74881\",\n PoolResources = \"fd617a\",\n PoolDamage = \"93f4a0\",\n PoolHorror = \"7bd2a0\",\n PoolClues = \"3b2550\",\n PoolDoom = \"16fcd6\",\n Playermat = \"0840d5\",\n ResourceCounter = \"a4b60d\",\n TokenDiscardZone = \"457de6\",\n TokenRemover = \"39b175\",\n Trash = \"4b8594\"\n },\n Mythos = {\n AdditionalPlayerCardsBag = \"2cba6b\",\n AllCardsBag = \"15bb07\",\n BlessCurseManager = \"5933fb\",\n CampaignThePathToCarcosa = \"aca04c\",\n ChaosBagZone = \"83ef06\",\n DataHelper = \"708279\",\n DeckImporter = \"a28140\",\n DoomCounter = \"85c4c6\",\n DoomInPlayCounter = \"652ff3\",\n InvestigatorCounter = \"f182ee\",\n MasterClueCounter = \"4a3aa4\",\n MythosArea = \"9f334f\",\n NavigationOverlayHandler = \"797ede\",\n OptionPanelSource = \"830bd0\",\n PlaceholderBoxDummy = \"a93466\",\n PlayArea = \"721ba2\",\n PlayAreaImageSelector = \"b7b45b\",\n PlayAreaZone = \"a2f932\",\n PlayerCardPanel = \"2d30ee\",\n ResourceTokenBag = \"9fadf9\",\n RulesReference = \"d99993\",\n SoundCube = \"3c988f\",\n TokenArranger = \"022907\",\n TokenSource = \"124381\",\n TokenSpawnTracker = \"e3ffc9\",\n TourStarter = \"0e5aa8\",\n Trash = \"70b9f6\",\n VictoryDisplay = \"6ccd6d\"\n }\n}\n\nlocal editsToIndex = {\n White = {},\n Orange = {},\n Green = {},\n Red = {},\n Mythos = {}\n}\n\n-- save function to keep edits to the index\nfunction onSave() return JSON.encode({ editsToIndex = editsToIndex }) end\n\n-- load function to restore edits to the index\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n editsToIndex = loadedData.editsToIndex\n updateMainIndex()\n end\nend\n\n-- merges the main index and the edits\nfunction updateMainIndex()\n for owner, subTable in pairs(editsToIndex) do\n for type, guid in pairs(subTable) do\n GuidReferences[owner][type] = guid\n end\n end\nend\n\n-- returns an object reference for the requested owner and type\nfunction getObjectByOwnerAndType(params)\n local owner = params.owner or \"Mythos\"\n local type = params.type\n return getObjectFromGUID(GuidReferences[owner][type])\nend\n\n-- returns a list of objects for the requested type\nfunction getObjectsByType(type)\n local objList = {}\n for owner, objects in pairs(GuidReferences) do\n local obj = getObjectFromGUID(objects[type])\n if obj then\n objList[owner] = obj\n end\n end\n return objList\nend\n\n-- returns a list of objects for the requested owner\nfunction getObjectsByOwner(owner)\n local objList = {}\n for type, guid in pairs(GuidReferences[owner]) do\n local obj = getObjectFromGUID(guid)\n if obj then\n objList[type] = obj\n end\n end\n return objList\nend\n\n-- makes an edit to the main index\nfunction editIndex(params)\n editsToIndex[params.owner][params.type] = params.guid\n updateMainIndex()\nend\n\n-- Returns the owner of the provided object (either the matColor or \"Mythos\")\n---@param object tts__GameObject Object to check\nfunction getOwnerOfObject(object)\n if object == nil then return end\n\n -- use GUID to check owners instead of obtaining each as reference\n local objectGuid = object.getGUID()\n\n -- check if object is directly owned\n for owner, subtable in pairs(GuidReferences) do\n for type, guid in pairs(subtable) do\n if guid == objectGuid then\n return owner\n end\n end\n end\n\n -- check if the object is in a handzone\n for owner, subtable in pairs(GuidReferences) do\n for type, guid in pairs(subtable) do\n for _, zone in ipairs(object.getZones()) do\n if guid == zone.getGUID() then\n return owner\n end\n end\n end\n end\n\n -- check if it is on an owned object\n local result = searchLib.belowPosition(object.getPosition())\n\n for owner, subtable in pairs(GuidReferences) do\n for type, guid in pairs(subtable) do\n for _, searchObj in ipairs(result) do\n if guid == searchObj.getGUID() then\n return owner\n end\n end\n end\n end\n\n -- default to \"Mythos\"\n return \"Mythos\"\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"editsToIndex\":{\"Green\":[],\"Mythos\":[],\"Orange\":[],\"Red\":[],\"White\":[]}}", "MeasureMovement": false, "Name": "go_game_piece_white", @@ -612,6 +607,51 @@ "Value": 0, "XmlUI": "" }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.66176, + "g": 0.66176, + "r": 0.66176 + }, + "Description": "This object contains \"Game Keys\" that can be assigned via Options --\u003e Game Keys.", + "DragSelectable": true, + "GMNotes": "", + "GUID": "fce69c", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": true, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/VictoryDisplayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local VictoryDisplayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getVictoryDisplay()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"VictoryDisplay\")\n end\n\n -- triggers an update of the victory count\n ---@param delay? number Delay in seconds after which the update call is executed\n VictoryDisplayApi.update = function(delay)\n getVictoryDisplay().call(\"startUpdate\", delay)\n end\n\n -- moves a card to the victory display (in the first empty spot)\n ---@param object tts__Object Object that should be checked and potentially moved\n VictoryDisplayApi.placeCard = function(object)\n if object ~= nil and object.tag == \"Card\" then\n getVictoryDisplay().call(\"placeCard\", object)\n end\n end\n\n return VictoryDisplayApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GameKeyHandler\")\nend)\n__bundle_register(\"core/GameKeyHandler\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal victoryDisplayApi = require(\"core/VictoryDisplayApi\")\n\nfunction onLoad()\n addHotkey(\"Add doom to agenda\", addDoomToAgenda)\n addHotkey(\"Add Bless/Curse context menu\", addBlurseSealingMenu)\n addHotkey(\"Discard object\", discardObject)\n addHotkey(\"Discard top card\", discardTopDeck)\n addHotkey(\"Display Bless/Curse status\", showBlessCurseStatus)\n addHotkey(\"Move card to Victory Display\", moveCardToVictoryDisplay)\n addHotkey(\"Place card into threat area\", takeCardIntoThreatArea)\n addHotkey(\"Remove a use\", removeOneUse)\n addHotkey(\"Reshuffle encounter deck\", mythosAreaApi.reshuffleEncounterDeck)\n addHotkey(\"Switch seat clockwise\", switchSeatClockwise)\n addHotkey(\"Switch seat counter-clockwise\", switchSeatCounterClockwise)\n addHotkey(\"Take clue from location\", takeClueFromLocation)\n addHotkey(\"Take clue from location (White)\", takeClueFromLocationWhite)\n addHotkey(\"Take clue from location (Orange)\", takeClueFromLocationOrange)\n addHotkey(\"Take clue from location (Green)\", takeClueFromLocationGreen)\n addHotkey(\"Take clue from location (Red)\", takeClueFromLocationRed)\n addHotkey(\"Upkeep\", triggerUpkeep)\n addHotkey(\"Upkeep (Multi-handed)\", triggerUpkeepMultihanded)\nend\n\n-- triggers the \"Upkeep\" function of the calling player's playermat\nfunction triggerUpkeep(playerColor)\n if playerColor == \"Black\" then\n broadcastToColor(\"Triggering 'Upkeep (Multihanded)' instead\", playerColor, \"Yellow\")\n triggerUpkeepMultihanded(playerColor)\n return\n end\n local matColor = playermatApi.getMatColor(playerColor)\n playermatApi.doUpkeepFromHotkey(matColor, playerColor)\nend\n\n-- triggers the \"Upkeep\" function of the calling player's playermat AND\n-- for all playermats that don't have a seated player, but an investigator card\nfunction triggerUpkeepMultihanded(playerColor)\n if playerColor ~= \"Black\" then\n triggerUpkeep(playerColor)\n end\n local colors = Player.getAvailableColors()\n for _, handColor in ipairs(colors) do\n local matColor = playermatApi.getMatColor(handColor)\n if playermatApi.returnInvestigatorId(matColor) ~= \"00000\" and Player[handColor].seated == false then\n playermatApi.doUpkeepFromHotkey(matColor, playerColor)\n end\n end\nend\n\n-- adds 1 doom to the agenda\nfunction addDoomToAgenda()\n local doomCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomCounter\")\n doomCounter.call(\"addVal\", 1)\nend\n\n-- move the hovered object to the nearest empty slot on the playermat\nfunction takeCardIntoThreatArea(playerColor, hoveredObject)\n -- only continue if an unlocked card\n if hoveredObject == nil\n or hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Deck\"\n or hoveredObject.hasTag(\"Location\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a non-location card and try again.\", playerColor, \"Yellow\")\n return\n end\n\n local matColor = playermatApi.getMatColor(playerColor)\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- do not continue if the threat area is already full\n local threatAreaPos = playermatApi.getEncounterCardDrawPosition(matColor, false)\n if threatAreaPos == playermatApi.getEncounterCardDrawPosition(matColor, true) then\n broadcastToColor(\"Threat area is full.\", playerColor, \"Yellow\")\n return\n end\n\n -- initialize list of objects to move (and store local positions)\n local additionalObjects = {}\n for _, obj in ipairs(searchLib.onObject(hoveredObject, \"isTileOrToken\")) do\n local data = {}\n data.object = obj\n data.localPos = hoveredObject.positionToLocal(obj.getPosition())\n table.insert(additionalObjects, data)\n end\n\n -- find out if the original card is on the green or red playermats\n local originalMatColor = guidReferenceApi.getOwnerOfObject(hoveredObject)\n\n -- determine modifiers for the playermats\n local modifierY = 0\n if originalMatColor == \"Red\" then\n modifierY = 90\n elseif originalMatColor == \"Green\" then\n modifierY = -90\n end\n\n local cardName = hoveredObject.getName()\n if cardName == \"\" then cardName = \"card\" end\n broadcastToAll(\"Moved \" .. cardName .. \" to \" .. getColoredName(playerColor) .. \"'s threat area.\", \"White\")\n\n -- get new rotation (rounded)\n local cardRot = hoveredObject.getRotation()\n local roundedRotY = roundToMultiple(cardRot.y, 45)\n local deltaRotY = 270 - mat.getRotation().y - modifierY\n local newCardRot = cardRot:setAt(\"y\", roundedRotY - deltaRotY)\n\n -- move the main card to threat area\n hoveredObject.setRotation(newCardRot)\n hoveredObject.setPosition(threatAreaPos)\n\n -- move tokens/tiles (to new global position)\n for _, data in ipairs(additionalObjects) do\n if not data.object.locked then\n data.object.setPosition(hoveredObject.positionToWorld(data.localPos))\n data.object.setRotation(data.object.getRotation() - Vector(0, deltaRotY, 0))\n end\n end\nend\n\n-- discard the hovered or selected objects to the respective trashcan and discard tokens on it if it was a card\nfunction discardObject(playerColor, hoveredObject)\n -- if more than one object is selected, discard them all, one at a time\n local selectedObjects = Player[playerColor].getSelectedObjects()\n if #selectedObjects \u003e 0 then\n discardGroup(playerColor, selectedObjects)\n return\n -- only continue if an unlocked card, deck or tile was hovered\n elseif hoveredObject == nil\n or (hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Deck\" and hoveredObject.type ~= \"Tile\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a token/tile or a card/deck and try again.\", playerColor, \"Yellow\")\n return\n end\n\n -- These should probably not be discarded normally. Ask player for confirmation.\n local tokenData = mythosAreaApi.returnTokenData()\n local scenarioName = tokenData.currentScenario\n if scenarioName ~= \"Lost in Time and Space\" and scenarioName ~= \"The Secret Name\" then\n if hoveredObject.type == \"Deck\" or hoveredObject.hasTag(\"Location\") then\n local suspect = (hoveredObject.type == \"Deck\") and \"Deck\" or \"Location\"\n Player[playerColor].showConfirmDialog(\"Discard \" .. suspect .. \"?\",\n function() performDiscard(playerColor, hoveredObject) end)\n return\n end\n end\n\n performDiscard(playerColor, hoveredObject)\nend\n\n-- actually performs the discarding of the object and tokens / tiles on it\nfunction performDiscard(playerColor, hoveredObject)\n -- initialize list of objects to discard\n local discardTheseObjects = { hoveredObject }\n\n -- discard tokens / tiles on cards / decks\n if hoveredObject.type ~= \"Tile\" then\n for _, obj in ipairs(searchLib.onObject(hoveredObject, \"isTileOrToken\")) do\n table.insert(discardTheseObjects, obj)\n end\n end\n\n local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor)\n playermatApi.discardListOfObjects(discardForMatColor, discardTheseObjects)\nend\n\nfunction discardGroup(playerColor, selectedObjects)\n local count = #selectedObjects\n -- discarding one at a time avoids an error with cards in the discard pile losing the 'hands' toggle and uses multiple mats\n for i = count, 1, -1 do\n Wait.time(function()\n if (selectedObjects[i].type == \"Card\" or selectedObjects[i].type ~= \"Deck\" or selectedObjects[i].type == \"Tile\") then\n performDiscard(playerColor, selectedObjects[i])\n end\n end, (count - i + 1) * 0.1)\n end\nend\n\n-- discard the top card of hovered deck, calling discardObject function\nfunction discardTopDeck(playerColor, hoveredObject)\n -- only continue if an unlocked card or deck was hovered\n if hoveredObject == nil\n or (hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Deck\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a deck/card and try again.\", playerColor, \"Yellow\")\n return\n end\n\n -- take top card from deck (unless it is already a single card)\n local takenCard = hoveredObject\n if hoveredObject.type == \"Deck\" then\n takenCard = hoveredObject.takeObject({ index = 0 })\n end\n Wait.frames(function() performDiscard(playerColor, takenCard) end, 1)\nend\n\n-- helper function to get the player to trigger the discard function for\nfunction getColorToDiscardFor(hoveredObject, playerColor)\n local pos = hoveredObject.getPosition()\n local closestMatColor = playermatApi.getMatColorByPosition(pos)\n\n -- check if actually on the closest playermat\n local closestMat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local bounds = closestMat.getBounds()\n\n -- define the area \"near\" the playermat\n local bufferAroundPlayermat = 2\n local areaNearPlayermat = {}\n areaNearPlayermat.minX = bounds.center.x - bounds.size.x / 2 - bufferAroundPlayermat\n areaNearPlayermat.maxX = bounds.center.x + bounds.size.x / 2 + bufferAroundPlayermat\n areaNearPlayermat.minZ = bounds.center.z - bounds.size.z / 2 - bufferAroundPlayermat\n areaNearPlayermat.maxZ = bounds.center.z + bounds.size.z / 2 + bufferAroundPlayermat\n\n -- discard to closest mat if near it\n if inArea(pos, areaNearPlayermat) then\n return closestMatColor\n end\n\n -- discard to closest mat if card is in a hand\n local handZone = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"HandZone\")\n for _, zone in ipairs(hoveredObject.getZones()) do\n if zone == handZone then\n return closestMatColor\n end\n end\n\n -- discard to triggering mat if previous conditions weren't met\n return playermatApi.getMatColor(playerColor)\nend\n\n-- moves the hovered card to the victory display\nfunction moveCardToVictoryDisplay(_, hoveredObject)\n victoryDisplayApi.placeCard(hoveredObject)\nend\n\n-- removes a use from a card (or a token if hovered)\nfunction removeOneUse(playerColor, hoveredObject)\n -- only continue if an unlocked card or tile was hovered\n if hoveredObject == nil\n or (hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Tile\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a token/tile or a card and try again.\", playerColor, \"Yellow\")\n return\n end\n\n local targetObject = nil\n\n -- discard hovered token / tile\n if hoveredObject.type == \"Tile\" then\n targetObject = hoveredObject\n elseif hoveredObject.type == \"Card\" then\n -- grab the first use type from the metadata (or nil)\n local notes = JSON.decode(hoveredObject.getGMNotes()) or {}\n local usesData = notes.uses or {}\n local useInfo = usesData[1] or {}\n local searchForType = useInfo.type\n if searchForType then searchForType = searchForType:lower() end\n\n for _, obj in ipairs(searchLib.onObject(hoveredObject, \"isTileOrToken\")) do\n if not obj.locked and obj.memo ~= \"resourceCounter\" then\n -- check for matching object, otherwise use the first hit\n if obj.memo and obj.memo == searchForType then\n targetObject = obj\n break\n elseif not targetObject then\n targetObject = obj\n end\n end\n end\n end\n\n -- release sealed token if card has one and no uses\n if tokenChecker.isChaosToken(targetObject) and hoveredObject.hasTag(\"CardThatSeals\") then\n local func = hoveredObject.getVar(\"releaseOneToken\") -- check if function exists\n if func ~= nil then\n hoveredObject.call(\"releaseOneToken\", playerColor)\n return\n end\n end\n\n -- error handling\n if not targetObject then\n broadcastToColor(\"No tokens found!\", playerColor, \"Yellow\")\n return\n end\n\n -- handling for stacked tokens\n if targetObject.getQuantity() \u003e 1 then\n targetObject = targetObject.takeObject()\n end\n\n -- feedback message\n local tokenName = targetObject.getName()\n if tokenName == \"\" then\n if targetObject.memo ~= \"\" then\n -- name handling for clue / doom\n if targetObject.memo == \"clueDoom\" then\n if targetObject.is_face_down then\n tokenName = \"Doom\"\n else\n tokenName = \"Clue\"\n end\n else\n tokenName = titleCase(targetObject.memo)\n end\n else\n tokenName = \"Unknown\"\n end\n end\n\n broadcastToAll(getColoredName(playerColor) .. \" removed a token: \" .. tokenName, playerColor)\n\n local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor)\n playermatApi.discardListOfObjects(discardForMatColor, { targetObject })\nend\n\n-- switches the triggering player to the next seat (clockwise)\nfunction switchSeatClockwise(playerColor)\n switchSeat(playerColor, \"clockwise\")\nend\n\n-- switches the triggering player to the next seat (counter-clockwise)\nfunction switchSeatCounterClockwise(playerColor)\n switchSeat(playerColor, \"counter-clockwise\")\nend\n\n-- handles seat switching in the given direction\nfunction switchSeat(playerColor, direction)\n if playerColor == \"Black\" or playerColor == \"Grey\" then\n broadcastToColor(\"This hotkey is only available to seated players.\", playerColor, \"Orange\")\n return\n end\n\n -- sort function for matcolors based on hand position (Green, White, Orange, Red)\n local function sortByHandPosition(color1, color2)\n local pos1 = Player[color1].getHandTransform().position\n local pos2 = Player[color2].getHandTransform().position\n return pos1.z \u003e pos2.z\n end\n\n -- get used playermats\n local usedColors = playermatApi.getUsedMatColors()\n table.sort(usedColors, sortByHandPosition)\n\n -- get current seat index\n local index\n for i, color in ipairs(usedColors) do\n if color == playerColor then\n index = i\n break\n end\n end\n if not index then\n broadcastToColor(\"Couldn't detect investigator.\", playerColor, \"Orange\")\n return\n end\n\n -- get next color\n index = index + ((direction == \"clockwise\") and -1 or 1)\n if index == 0 then\n index = #usedColors\n elseif index \u003e #usedColors then\n index = 1\n end\n\n -- swap color\n navigationOverlayApi.loadCamera(Player[playerColor], usedColors[index])\nend\n\nfunction takeClueFromLocationWhite(_, hoveredObject)\n takeClueFromLocation(\"White\", hoveredObject)\nend\n\nfunction takeClueFromLocationOrange(_, hoveredObject)\n takeClueFromLocation(\"Orange\", hoveredObject)\nend\n\nfunction takeClueFromLocationGreen(_, hoveredObject)\n takeClueFromLocation(\"Green\", hoveredObject)\nend\n\nfunction takeClueFromLocationRed(_, hoveredObject)\n takeClueFromLocation(\"Red\", hoveredObject)\nend\n\n-- takes a clue from a location, player needs to hover the clue directly or the location\nfunction takeClueFromLocation(playerColor, hoveredObject)\n -- use different color for messages if player is not seated (because this hotkey is called for a different mat)\n local messageColor = playerColor\n if not Player[playerColor] or not Player[playerColor].seated then\n messageColor = getFirstSeatedPlayer()\n end\n\n local cardName, clue\n\n if hoveredObject == nil then\n broadcastToColor(\"Hover a clue or card with clues and try again.\", messageColor, \"Yellow\")\n return\n elseif hoveredObject.type == \"Card\" then\n cardName = hoveredObject.getName()\n local searchResult = searchLib.onObject(hoveredObject, \"isClue\")\n\n if #searchResult == 0 then\n broadcastToColor(\"This card does not have any clues on it.\", messageColor, \"Yellow\")\n return\n else\n clue = searchResult[1]\n end\n elseif hoveredObject.memo == \"clueDoom\" then\n if hoveredObject.is_face_down then\n broadcastToColor(\"This is a doom token and not a clue.\", messageColor, \"Yellow\")\n return\n end\n\n clue = hoveredObject\n local searchResult = searchLib.belowPosition(clue.getPosition(), \"isCard\")\n\n if #searchResult ~= 0 then\n cardName = searchResult[1].getName()\n end\n elseif hoveredObject.type == \"Infinite\" and hoveredObject.getName() == \"Clue tokens\" then\n clue = hoveredObject.takeObject()\n cardName = \"token pool\"\n else\n broadcastToColor(\"Hover a clue or card with clues and try again.\", messageColor, \"Yellow\")\n return\n end\n\n local clickableClues = optionPanelApi.getOptions()[\"useClueClickers\"]\n\n -- handling for calling this for a specific mat via hotkey\n local matColor, pos\n if Player[playerColor] and Player[playerColor].seated then\n matColor = playermatApi.getMatColor(playerColor)\n else\n matColor = playerColor\n end\n\n if clickableClues then\n pos = { x = 0.49, y = 2.66, z = 0.00 }\n playermatApi.updateCounter(matColor, \"ClickableClueCounter\", _, 1)\n else\n pos = playermatApi.transformLocalPosition({ x = -1.12, y = 0.05, z = 0.7 }, matColor)\n end\n\n local rot = playermatApi.returnRotation(matColor)\n\n -- check if found clue is a stack or single token\n if clue.getQuantity() \u003e 1 then\n clue.takeObject({ position = pos, rotation = rot })\n else\n clue.setPositionSmooth(pos)\n clue.setRotation(rot)\n end\n\n if cardName then\n broadcastToAll(getColoredName(playerColor) .. \" took one clue from \" .. cardName .. \".\", \"White\")\n else\n broadcastToAll(getColoredName(playerColor) .. \" took one clue.\", \"White\")\n end\n\n victoryDisplayApi.update()\nend\n\n-- broadcasts the bless/curse status to the calling player\nfunction showBlessCurseStatus(playerColor)\n blessCurseManagerApi.broadcastStatus(playerColor)\nend\n\n-- adds Wendy's menu to the hovered card\nfunction addBlurseSealingMenu(playerColor, hoveredObject)\n blessCurseManagerApi.addBlurseSealingMenu(playerColor, hoveredObject)\nend\n\n-- Simple method to check if the given point is in a specified area\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within\nfunction inArea(point, bounds)\n return (point.x \u003e bounds.minX\n and point.x \u003c bounds.maxX\n and point.z \u003e bounds.minZ\n and point.z \u003c bounds.maxZ)\nend\n\n-- capitalizes the first letter\nfunction titleCase(str)\n local first = str:sub(1, 1)\n local rest = str:sub(2)\n return first:upper() .. rest:lower()\nend\n\n-- returns the color of the first seated player\nfunction getFirstSeatedPlayer()\n for _, color in ipairs(getSeatedPlayers()) do\n return color\n end\nend\n\n-- returns the colored steam name or color\nfunction getColoredName(playerColor)\n local displayName = playerColor\n if Player[playerColor].steam_name then\n displayName = Player[playerColor].steam_name\n end\n\n -- add bb-code\n return \"[\" .. Color.fromString(playerColor):toHex() .. \"]\" .. displayName .. \"[-]\"\nend\n\nfunction roundToMultiple(num, mult)\n return math.floor((num + mult / 2) / mult) * mult\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "go_game_piece_white", + "Nickname": "Game Key Handler", + "Snap": true, + "Sticky": true, + "Tooltip": true, + "Transform": { + "posX": 78, + "posY": 1.328, + "posZ": -10, + "rotX": 0, + "rotY": 0, + "rotZ": 0, + "scaleX": 1, + "scaleY": 1, + "scaleZ": 1 + }, + "Value": 0, + "XmlUI": "" + }, { "AltLookAngle": { "x": 0, @@ -635,7 +675,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/token/TokenSpawnTracker\")\nend)\n__bundle_register(\"core/token/TokenSpawnTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal spawnedCardGuids = {}\n\nfunction onSave() return JSON.encode({ cards = spawnedCardGuids }) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData) or {}\n spawnedCardGuids = loadedData.cards or {}\n end\n\n -- context menu entries\n self.addContextMenuItem(\"Reset All\", resetAll)\n self.addContextMenuItem(\"Reset Locations\", resetAllLocations)\n self.addContextMenuItem(\"Reset Player Cards\", resetAllAssetAndEvents)\nend\n\nfunction hasSpawnedTokens(cardGuid)\n return spawnedCardGuids[cardGuid] == true\nend\n\nfunction markTokensSpawned(cardGuid)\n spawnedCardGuids[cardGuid] = true\nend\n\nfunction resetTokensSpawned(cardGuid)\n spawnedCardGuids[cardGuid] = nil\nend\n\nfunction resetAll() spawnedCardGuids = {} end\n\nfunction resetAllLocations() resetSpecificTypes(\"Location\") end\n\nfunction resetAllAssetAndEvents() resetSpecificTypes(\"Asset\", \"Event\") end\n\nfunction resetSpecificTypes(type1, type2)\n local resetList = {}\n for cardGuid, _ in pairs(spawnedCardGuids) do\n local card = getObjectFromGUID(cardGuid)\n if card ~= nil then\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n -- Check this by type rather than the PlayerCard tag so we don't reset weaknesses\n if cardMetadata.type == type1 or cardMetadata.type == type2 then\n resetList[cardGuid] = true\n end\n end\n end\n for cardGuid, _ in pairs(resetList) do\n spawnedCardGuids[cardGuid] = nil\n end\nend\n\n-- Listener to reset card token spawns when they enter a hand.\nfunction onObjectEnterZone(zone, enterObject)\n if zone.type == \"Hand\" and enterObject.type == \"Card\" then\n resetTokensSpawned(enterObject.getGUID())\n end\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/token/TokenSpawnTracker\")\nend)\n__bundle_register(\"core/token/TokenSpawnTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal spawnedCardGuids = {}\n\nfunction onSave() return JSON.encode({ cards = spawnedCardGuids }) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData) or {}\n spawnedCardGuids = loadedData.cards or {}\n end\n\n -- context menu entries\n self.addContextMenuItem(\"Reset All\", resetAll)\n self.addContextMenuItem(\"Reset Locations\", resetAllLocations)\n self.addContextMenuItem(\"Reset Player Cards\", resetAllAssetAndEvents)\nend\n\nfunction hasSpawnedTokens(cardGuid)\n return spawnedCardGuids[cardGuid] == true\nend\n\nfunction markTokensSpawned(cardGuid)\n spawnedCardGuids[cardGuid] = true\nend\n\nfunction resetTokensSpawned(card)\n spawnedCardGuids[card.getGUID()] = nil\nend\n\nfunction resetAll() spawnedCardGuids = {} end\n\nfunction resetAllLocations() resetSpecificTypes(\"Location\") end\n\nfunction resetAllAssetAndEvents() resetSpecificTypes(\"Asset\", \"Event\") end\n\nfunction resetSpecificTypes(type1, type2)\n local resetList = {}\n for cardGuid, _ in pairs(spawnedCardGuids) do\n local card = getObjectFromGUID(cardGuid)\n if card ~= nil then\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n -- Check this by type rather than the PlayerCard tag so we don't reset weaknesses\n if cardMetadata.type == type1 or cardMetadata.type == type2 then\n resetList[cardGuid] = true\n end\n end\n end\n for cardGuid, _ in pairs(resetList) do\n spawnedCardGuids[cardGuid] = nil\n end\nend\n\n-- Listener to reset card token spawns when they enter a hand.\nfunction onObjectEnterZone(zone, enterObject)\n if zone.type == \"Hand\" and enterObject.type == \"Card\" then\n resetTokensSpawned(enterObject)\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"cards\":[]}", "MeasureMovement": false, "Name": "Checker_white", @@ -845,6 +885,102 @@ "Value": 0, "XmlUI": "" }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "a": 0.5098, + "b": 1, + "g": 1, + "r": 1 + }, + "Description": "", + "DragSelectable": true, + "GMNotes": "", + "GUID": "a2f932", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": true, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "ScriptingTrigger", + "Nickname": "", + "Snap": true, + "Sticky": true, + "Tooltip": true, + "Transform": { + "posX": -27.94, + "posY": 3.5, + "posZ": 0, + "rotX": 0, + "rotY": 0, + "rotZ": 0, + "scaleX": 37, + "scaleY": 4, + "scaleZ": 37 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "a": 0.75, + "b": 0.168, + "g": 0.701, + "r": 0.192 + }, + "Description": "", + "DragSelectable": true, + "FogColor": "Green", + "FogHidePointers": false, + "FogReverseHiding": false, + "FogSeethrough": true, + "GMNotes": "", + "GUID": "3aab97", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": true, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "FogOfWarTrigger", + "Nickname": "", + "Snap": true, + "Sticky": true, + "Tooltip": true, + "Transform": { + "posX": -21.648, + "posY": 0.87, + "posZ": 22.438, + "rotX": 0, + "rotY": 180, + "rotZ": 0, + "scaleX": 2.8, + "scaleY": 0.55, + "scaleZ": 3.8 + }, + "Value": 0, + "XmlUI": "" + }, { "AltLookAngle": { "x": 0, @@ -1076,7 +1212,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "tableHeightOffset =-9\nfunction onSave()\n saved_data = JSON.encode({tid=tableImageData, cd=checkData})\n --saved_data = \"\"\n return saved_data\nend\n\nfunction onload(saved_data)\n --Loads the tracking for if the game has started yet\n if saved_data ~= \"\" then\n local loaded_data = JSON.decode(saved_data)\n tableImageData = loaded_data.tid\n checkData = loaded_data.cd\n else\n tableImageData = {}\n checkData = {move=false, scale=false}\n end\n\n --Disables interactable status of objects with GUID in list\n for _, guid in ipairs(ref_noninteractable) do\n local obj = getObjectFromGUID(guid)\n if obj then obj.interactable = false end\n end\n\n --Establish references to table parts\n obj_leg1 = getObjectFromGUID(\"afc863\")\n obj_leg2 = getObjectFromGUID(\"c8edca\")\n obj_leg3 = getObjectFromGUID(\"393bf7\")\n obj_leg4 = getObjectFromGUID(\"12c65e\")\n obj_surface = getObjectFromGUID(\"4ee1f2\")\n obj_side_top = getObjectFromGUID(\"35b95f\")\n obj_side_bot = getObjectFromGUID(\"f938a2\")\n obj_side_lef = getObjectFromGUID(\"9f95fd\")\n obj_side_rig = getObjectFromGUID(\"5af8f2\")\n\n controlActive = true\n createOpenCloseButton()\nend\n\n\n\n--Activation/deactivation of control panel\n\n\n\n--Activated by clicking on\nfunction click_toggleControl(_, color)\n if permissionCheck(color) then\n if not controlActive then\n --Activate control panel\n controlActive = true\n self.clearButtons()\n createOpenCloseButton()\n createSurfaceInput()\n createSurfaceButtons()\n createScaleInput()\n createScaleButtons()\n else\n --Deactivate control panel\n controlActive = false\n self.clearButtons()\n self.clearInputs()\n createOpenCloseButton()\n\n end\n end\nend\n\n\n\n\n--Table surface control\n\n\n\n--Changes table surface\nfunction click_applySurface(_, color)\n if permissionCheck(color) then\n updateSurface()\n broadcastToAll(\"New Table Image Applied\", {0.2,0.9,0.2})\n end\nend\n\n--Saves table surface\nfunction click_saveSurface(_, color)\n if permissionCheck(color) then\n local nickname = self.getInputs()[1].value\n local url = self.getInputs()[2].value\n if nickname == \"\" then\n --No nickname\n broadcastToAll(\"Please supply a nickname for this save.\", {0.9,0.2,0.2})\n else\n --Nickname exists\n\n if findInImageDataIndex(url, nickname) == nil then\n --Save doesn't exist already\n table.insert(tableImageData, {url=url, name=nickname})\n broadcastToAll(\"Image URL saved to memory.\", {0.2,0.9,0.2})\n --Refresh buttons\n self.clearButtons()\n createOpenCloseButton()\n createSurfaceButtons()\n createScaleButtons()\n else\n --Save exists already\n broadcastToAll(\"Memory already contains a save with this Name or URL. Delete it first.\", {0.9,0.2,0.2})\n end\n end\n end\nend\n\n--Loads table surface\nfunction click_loadMemory(_, color, index)\n if permissionCheck(color) then\n self.editInput({index=0, value=tableImageData[index].name})\n self.editInput({index=1, value=tableImageData[index].url})\n updateSurface()\n broadcastToAll(\"Table Image Loaded\", {0.2,0.9,0.2})\n end\nend\n\n--Deletes table surface\nfunction click_deleteMemory(_, color, index)\n if permissionCheck(color) then\n table.remove(tableImageData, index)\n self.clearButtons()\n createOpenCloseButton()\n createSurfaceButtons()\n createScaleButtons()\n broadcastToAll(\"Element Removed from Memory\", {0.2,0.9,0.2})\n end\nend\n\n--Updates surface from the values in the input field\nfunction updateSurface()\n local customInfo = obj_surface.getCustomObject()\n customInfo.diffuse = self.getInputs()[2].value\n obj_surface.setCustomObject(customInfo)\n obj_surface = obj_surface.reload()\nend\n\n\n\n--Table Scale control\n\n\n\n--Applies Scale to table pieces\nfunction click_applyScale(_, color)\n if permissionCheck(color) then\n local newWidth = tonumber(self.getInputs()[3].value)\n local newDepth = tonumber(self.getInputs()[4].value)\n if type(newWidth) ~= \"number\" then\n broadcastToAll(\"Invalid Width\", {0.9,0.2,0.2})\n return\n elseif type(newDepth) ~= \"number\" then\n broadcastToAll(\"Invalid Depth\", {0.9,0.2,0.2})\n return\n elseif newWidth\u003c0.1 or newDepth\u003c0.1 then\n broadcastToAll(\"Scale cannot go below 0.1\", {0.9,0.2,0.2})\n return\n elseif newWidth\u003e12 or newDepth\u003e12 then\n broadcastToAll(\"Scale should not go over 12 (world size limitation)\", {0.9,0.2,0.2})\n return\n else\n changeTableScale(math.abs(newWidth), math.abs(newDepth))\n broadcastToAll(\"Scale applied.\", {0.2,0.9,0.2})\n end\n end\nend\n\n--Checks/unchecks move box for hands\nfunction click_checkMove(_, color)\n if permissionCheck(color) then\n local find_func = function(o) return o.click_function==\"click_checkMove\" end\n if checkData.move == true then\n checkData.move = false\n local buttonEntry = findButton(self, find_func)\n self.editButton({index=buttonEntry.index, label=\"\"})\n else\n checkData.move = true\n local buttonEntry = findButton(self, find_func)\n self.editButton({index=buttonEntry.index, label=string.char(10008)})\n end\n end\nend\n\n--Checks/unchecks scale box for hands\n--This button was disabled for technical reasons\n--[[\nfunction click_checkScale(_, color)\n if permissionCheck(color) then\n local find_func = function(o) return o.click_function==\"click_checkScale\" end\n if checkData.scale == true then\n checkData.scale = false\n local buttonEntry = findButton(self, find_func)\n self.editButton({index=buttonEntry.index, label=\"\"})\n else\n checkData.scale = true\n local buttonEntry = findButton(self, find_func)\n self.editButton({index=buttonEntry.index, label=string.char(10008)})\n end\n end\nend\n]]\n\n--Alters scale of elements and moves them\nfunction changeTableScale(width, depth)\n --Scaling factors used to translate scale to position offset\n local width2pos = (width-1) * 18\n local depth2pos = (depth-1) * 18\n\n --Hand zone movement\n if checkData.move == true then\n for _, pc in ipairs(ref_playerColor) do\n if Player[pc].getHandCount() \u003e 0 then\n moveHandZone(Player[pc], width2pos, depth2pos)\n end\n end\n end\n --Hand zone scaling\n --The button to enable this was disabled for technical reasons\n if checkData.scale == true then\n for _, pc in ipairs(ref_playerColor) do\n if Player[pc].getHandCount() \u003e 0 then\n scaleHandZone(Player[pc], width, depth)\n end\n end\n end\n\n --Resizing table elements\n obj_side_top.setScale({width, 1, 1})\n obj_side_bot.setScale({width, 1, 1})\n obj_side_lef.setScale({depth, 1, 1})\n obj_side_rig.setScale({depth, 1, 1})\n obj_surface.setScale({width, 1, depth})\n\n --Moving table elements to accomodate new scale\n obj_side_lef.setPosition({-width2pos,tableHeightOffset,0})\n obj_side_rig.setPosition({ width2pos,tableHeightOffset,0})\n obj_side_top.setPosition({0,tableHeightOffset, depth2pos})\n obj_side_bot.setPosition({0,tableHeightOffset,-depth2pos})\n obj_leg1.setPosition({-width2pos,tableHeightOffset,-depth2pos})\n obj_leg2.setPosition({-width2pos,tableHeightOffset, depth2pos})\n obj_leg3.setPosition({ width2pos,tableHeightOffset, depth2pos})\n obj_leg4.setPosition({ width2pos,tableHeightOffset,-depth2pos})\n self.setPosition(obj_leg4.positionToWorld({-22.12, 8.74,-19.16}))\n --Only enabled when changing tableHeightOffset\n --obj_surface.setPosition({0,tableHeightOffset,0})\nend\n\n--Move hand zone, p=player reference, facts are scaling factors\nfunction moveHandZone(p, width2pos, depth2pos)\n local widthX = obj_side_rig.getPosition().x\n local depthZ = obj_side_top.getPosition().z\n for i=1, p.getHandCount() do\n local handT = p.getHandTransform()\n local pos = handT.position\n local y = handT.rotation.y\n\n if y\u003c45 or y\u003e320 or y\u003e135 and y\u003c225 then\n if pos.z \u003e 0 then\n pos.z = pos.z + depth2pos - depthZ\n else\n pos.z = pos.z - depth2pos + depthZ\n end\n else\n if pos.x \u003e 0 then\n pos.x = pos.x + width2pos - widthX\n else\n pos.x = pos.x - width2pos + widthX\n end\n end\n\n --Only enabled when changing tableHeightOffset\n --pos.y = tableHeightOffset + 14\n\n handT.position = pos\n p.setHandTransform(handT, i)\n end\nend\n\n\n---Scales hand zones, p=player reference, facts are scaling factors\nfunction scaleHandZone(p, width, depth)\n local widthFact = width / obj_side_top.getScale().x\n local depthFact = depth / obj_side_lef.getScale().x\n for i=1, p.getHandCount() do\n local handT = p.getHandTransform()\n local scale = handT.scale\n local y = handT.rotation.y\n if y\u003c45 or y\u003e320 or y\u003e135 and y\u003c225 then\n scale.x = scale.x * widthFact\n else\n scale.x = scale.x * depthFact\n end\n handT.scale = scale\n p.setHandTransform(handT, i)\n end\nend\n\n\n\n--Information gathering\n\n\n\n--Checks if a color is promoted or host\nfunction permissionCheck(color)\n if Player[color].host==true or Player[color].promoted==true then\n return true\n else\n return false\n end\nend\n\n--Locates a string saved within memory file\nfunction findInImageDataIndex(...)\n for _, str in ipairs({...}) do\n for i, v in ipairs(tableImageData) do\n if v.url == str or v.name == str then\n return i\n end\n end\n end\n return nil\nend\n\n--Round number (num) to the Nth decimal (dec)\nfunction round(num, dec)\n local mult = 10^(dec or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n--Locates a button with a helper function\nfunction findButton(obj, func)\n if func==nil then error(\"No func supplied to findButton\") end\n for _, v in ipairs(obj.getButtons()) do\n if func(v) then\n return v\n end\n end\n return nil\nend\n\n\n\n--Creation of buttons/inputs\n\n\n\nfunction createOpenCloseButton()\n local tooltip = \"Open Table Control Panel\"\n if controlActive then\n tooltip = \"Close Table Control Panel\"\n end\n self.createButton({\n click_function=\"click_toggleControl\", function_owner=self,\n position={0,0,0}, rotation={-45,0,0}, height=400, width=400,\n color={1,1,1,0}, tooltip=tooltip\n })\nend\n\nfunction createSurfaceInput()\n local currentURL = obj_surface.getCustomObject().diffuse\n local nickname = \"\"\n if findInImageDataIndex(currentURL) ~= nil then\n nickname = tableImageData[findInImageDataIndex(currentURL)].name\n end\n self.createInput({\n label=\"Nickname\", input_function=\"none\", function_owner=self,\n alignment=3, position={0,0,2}, height=224, width=4000,\n font_size=200, tooltip=\"Enter nickname for table image (only used for save)\",\n value=nickname\n })\n self.createInput({\n label=\"URL\", input_function=\"none\", function_owner=self,\n alignment=3, position={0,0,3}, height=224, width=4000,\n font_size=200, tooltip=\"Enter URL for tabletop image\",\n value=currentURL\n })\nend\n\nfunction createSurfaceButtons()\n --Label\n self.createButton({\n label=\"Tabletop Surface Image\", click_function=\"none\",\n position={0,0,1}, height=0, width=0, font_size=300, font_color={1,1,1}\n })\n --Functional\n self.createButton({\n label=\"Apply Image\\nTo Table\", click_function=\"click_applySurface\",\n function_owner=self, tooltip=\"Apply URL as table image\",\n position={2,0,4}, height=440, width=1400, font_size=200,\n })\n self.createButton({\n label=\"Save Image\\nTo Memory\", click_function=\"click_saveSurface\",\n function_owner=self, tooltip=\"Record URL into memory (requires nickname)\",\n position={-2,0,4}, height=440, width=1400, font_size=200,\n })\n --Label\n self.createButton({\n label=\"Load From Memory\", click_function=\"none\",\n position={0,0,5.5}, height=0, width=0, font_size=300, font_color={1,1,1}\n })\n --Saves, created dynamically from memory file\n for i, memoryEntry in ipairs(tableImageData) do\n --Load\n local funcName = i..\"loadMemory\"\n local func = function(x,y) click_loadMemory(x,y,i) end\n self.setVar(funcName, func)\n self.createButton({\n label=memoryEntry.name, click_function=funcName,\n function_owner=self, tooltip=memoryEntry.url, font_size=200,\n position={-0.6,0,6.5+0.5*(i-1)}, height=240, width=3300,\n })\n --Delete\n local funcName = i..\"deleteMemory\"\n local func = function(x,y) click_deleteMemory(x,y,i) end\n self.setVar(funcName, func)\n self.createButton({\n label=\"DELETE\", click_function=funcName,\n function_owner=self, tooltip=\"\",\n position={3.6,0,6.5+0.5*(i-1)}, height=240, width=600,\n font_size=160, font_color={1,0,0}, color={0.8,0.8,0.8}\n })\n end\nend\n\nfunction createScaleInput()\n self.createInput({\n label=string.char(8644), input_function=\"none\", function_owner=self,\n alignment=3, position={-8.5,0,2}, height=224, width=400,\n font_size=200, tooltip=\"Table Width\",\n value=round(obj_side_top.getScale().x, 1)\n })\n self.createInput({\n label=string.char(8645), input_function=\"none\", function_owner=self,\n alignment=3, position={-7.5,0,2}, height=224, width=400,\n font_size=200, tooltip=\"Table Depth\",\n value=round(obj_side_lef.getScale().x, 1)\n })\nend\n\nfunction createScaleButtons()\n --Labels\n self.createButton({\n label=\"Table Scale\", click_function=\"none\",\n position={-8,0,1}, height=0, width=0, font_size=300, font_color={1,1,1}\n })\n self.createButton({\n label=string.char(8644)..\" \"..string.char(8645),\n click_function=\"none\",\n position={-8,0,2}, height=0, width=0, font_size=300, font_color={1,1,1}\n })\n self.createButton({\n label=\"Move Hands:\", click_function=\"none\",\n position={-8.3,0,3}, height=0, width=0, font_size=200, font_color={1,1,1}\n })\n --Disabled due to me removing the feature for technical reasons\n --[[\n self.createButton({\n label=\"Scale Hands:\", click_function=\"none\",\n position={-8.3,0,4}, height=0, width=0, font_size=200, font_color={1,1,1}\n })\n ]]\n --Checkboxes\n local label = \"\"\n if checkData.move == true then label = string.char(10008) end\n self.createButton({\n label=label, click_function=\"click_checkMove\",\n function_owner=self, tooltip=\"Check to move hands when table is rescaled\",\n position={-6.8,0,3}, height=224, width=224, font_size=200,\n })\n --[[\n local label = \"\"\n if checkData.scale == true then label = string.char(10008) end\n self.createButton({\n label=label, click_function=\"click_checkScale\",\n function_owner=self, tooltip=\"Check to scale the width of hands when table is rescaled\",\n position={-6.8,0,4}, height=224, width=224, font_size=200,\n })\n ]]\n --Apply button\n self.createButton({\n label=\"Apply Scale\", click_function=\"click_applyScale\",\n function_owner=self, tooltip=\"Apply width/depth to table\",\n position={-8,0,4}, height=440, width=1400, font_size=200,\n })\nend\n\n\n\n\n\n--Data tables\n\n\n\n\nref_noninteractable = {\n \"afc863\",\"c8edca\",\"393bf7\",\"12c65e\",\"f938a2\",\"9f95fd\",\"35b95f\",\n \"5af8f2\",\"4ee1f2\",\"bd69bd\"\n}\n\nref_playerColor = {\n \"White\", \"Brown\", \"Red\", \"Orange\", \"Yellow\",\n \"Green\", \"Teal\", \"Blue\", \"Purple\", \"Pink\", \"Black\"\n}\n\n--Dummy function, absorbs unwanted triggers\nfunction none() end", + "LuaScript": "tableHeightOffset = -9\n\nfunction onSave()\n return JSON.encode({ tid = tableImageData, cd = checkData })\nend\n\nfunction onload(saved_data)\n if saved_data ~= \"\" then\n local loaded_data = JSON.decode(saved_data)\n tableImageData = loaded_data.tid\n checkData = loaded_data.cd\n else\n tableImageData = {}\n checkData = { move = false, scale = false }\n end\n\n --Disables interactable status of objects with GUID in list\n for _, guid in ipairs(ref_noninteractable) do\n local obj = getObjectFromGUID(guid)\n if obj then obj.interactable = false end\n end\n\n --Establish references to table parts\n obj_leg1 = getObjectFromGUID(\"afc863\")\n obj_leg2 = getObjectFromGUID(\"c8edca\")\n obj_leg3 = getObjectFromGUID(\"393bf7\")\n obj_leg4 = getObjectFromGUID(\"12c65e\")\n obj_surface = getObjectFromGUID(\"4ee1f2\")\n obj_side_top = getObjectFromGUID(\"35b95f\")\n obj_side_bot = getObjectFromGUID(\"f938a2\")\n obj_side_lef = getObjectFromGUID(\"9f95fd\")\n obj_side_rig = getObjectFromGUID(\"5af8f2\")\n\n controlActive = true\n createOpenCloseButton()\nend\n\n--Activation/deactivation of control panel\n\n--Activated by clicking on\nfunction click_toggleControl(_, color)\n if permissionCheck(color) then\n if not controlActive then\n --Activate control panel\n controlActive = true\n self.clearButtons()\n createOpenCloseButton()\n createSurfaceInput()\n createSurfaceButtons()\n createScaleInput()\n createScaleButtons()\n else\n --Deactivate control panel\n controlActive = false\n self.clearButtons()\n self.clearInputs()\n createOpenCloseButton()\n end\n end\nend\n\n--Table surface control\n\n--Changes table surface\nfunction click_applySurface(_, color)\n if permissionCheck(color) then\n updateSurface()\n broadcastToAll(\"New Table Image Applied\", { 0.2, 0.9, 0.2 })\n end\nend\n\n--Saves table surface\nfunction click_saveSurface(_, color)\n if permissionCheck(color) then\n local nickname = self.getInputs()[1].value\n local url = self.getInputs()[2].value\n if nickname == \"\" then\n --No nickname\n broadcastToAll(\"Please supply a nickname for this save.\", { 0.9, 0.2, 0.2 })\n else\n --Nickname exists\n\n if findInImageDataIndex(url, nickname) == nil then\n --Save doesn't exist already\n table.insert(tableImageData, { url = url, name = nickname })\n broadcastToAll(\"Image URL saved to memory.\", { 0.2, 0.9, 0.2 })\n --Refresh buttons\n self.clearButtons()\n createOpenCloseButton()\n createSurfaceButtons()\n createScaleButtons()\n else\n --Save exists already\n broadcastToAll(\"Memory already contains a save with this Name or URL. Delete it first.\", { 0.9, 0.2, 0.2 })\n end\n end\n end\nend\n\n--Loads table surface\nfunction click_loadMemory(_, color, index)\n if permissionCheck(color) then\n self.editInput({ index = 0, value = tableImageData[index].name })\n self.editInput({ index = 1, value = tableImageData[index].url })\n updateSurface()\n broadcastToAll(\"Table Image Loaded\", { 0.2, 0.9, 0.2 })\n end\nend\n\n--Deletes table surface\nfunction click_deleteMemory(_, color, index)\n if permissionCheck(color) then\n table.remove(tableImageData, index)\n self.clearButtons()\n createOpenCloseButton()\n createSurfaceButtons()\n createScaleButtons()\n broadcastToAll(\"Element Removed from Memory\", { 0.2, 0.9, 0.2 })\n end\nend\n\n--Updates surface from the values in the input field\nfunction updateSurface()\n local customInfo = obj_surface.getCustomObject()\n customInfo.diffuse = self.getInputs()[2].value\n obj_surface.setCustomObject(customInfo)\n obj_surface = obj_surface.reload()\nend\n\n--Table Scale control\n\n--Applies Scale to table pieces\nfunction click_applyScale(_, color)\n if permissionCheck(color) then\n local newWidth = tonumber(self.getInputs()[3].value)\n local newDepth = tonumber(self.getInputs()[4].value)\n if type(newWidth) ~= \"number\" then\n broadcastToAll(\"Invalid Width\", { 0.9, 0.2, 0.2 })\n return\n elseif type(newDepth) ~= \"number\" then\n broadcastToAll(\"Invalid Depth\", { 0.9, 0.2, 0.2 })\n return\n elseif newWidth \u003c 0.1 or newDepth \u003c 0.1 then\n broadcastToAll(\"Scale cannot go below 0.1\", { 0.9, 0.2, 0.2 })\n return\n elseif newWidth \u003e 12 or newDepth \u003e 12 then\n broadcastToAll(\"Scale should not go over 12 (world size limitation)\", { 0.9, 0.2, 0.2 })\n return\n else\n changeTableScale(math.abs(newWidth), math.abs(newDepth))\n broadcastToAll(\"Scale applied.\", { 0.2, 0.9, 0.2 })\n end\n end\nend\n\n--Checks/unchecks move box for hands\nfunction click_checkMove(_, color)\n if permissionCheck(color) then\n local find_func = function(o) return o.click_function == \"click_checkMove\" end\n if checkData.move == true then\n checkData.move = false\n local buttonEntry = findButton(self, find_func)\n self.editButton({ index = buttonEntry.index, label = \"\" })\n else\n checkData.move = true\n local buttonEntry = findButton(self, find_func)\n self.editButton({ index = buttonEntry.index, label = string.char(10008) })\n end\n end\nend\n\n--Checks/unchecks scale box for hands\n--This button was disabled for technical reasons\n--[[\nfunction click_checkScale(_, color)\n if permissionCheck(color) then\n local find_func = function(o) return o.click_function==\"click_checkScale\" end\n if checkData.scale == true then\n checkData.scale = false\n local buttonEntry = findButton(self, find_func)\n self.editButton({index=buttonEntry.index, label=\"\"})\n else\n checkData.scale = true\n local buttonEntry = findButton(self, find_func)\n self.editButton({index=buttonEntry.index, label=string.char(10008)})\n end\n end\nend\n]]\n\n--Alters scale of elements and moves them\nfunction changeTableScale(width, depth)\n --Scaling factors used to translate scale to position offset\n local width2pos = (width - 1) * 18\n local depth2pos = (depth - 1) * 18\n\n --Hand zone movement\n if checkData.move == true then\n for _, pc in ipairs(ref_playerColor) do\n if Player[pc].getHandCount() \u003e 0 then\n moveHandZone(Player[pc], width2pos, depth2pos)\n end\n end\n end\n --Hand zone scaling\n --The button to enable this was disabled for technical reasons\n if checkData.scale == true then\n for _, pc in ipairs(ref_playerColor) do\n if Player[pc].getHandCount() \u003e 0 then\n scaleHandZone(Player[pc], width, depth)\n end\n end\n end\n\n --Resizing table elements\n obj_side_top.setScale({ width, 1, 1 })\n obj_side_bot.setScale({ width, 1, 1 })\n obj_side_lef.setScale({ depth, 1, 1 })\n obj_side_rig.setScale({ depth, 1, 1 })\n obj_surface.setScale({ width, 1, depth })\n\n --Moving table elements to accomodate new scale\n obj_side_lef.setPosition({ -width2pos, tableHeightOffset, 0 })\n obj_side_rig.setPosition({ width2pos, tableHeightOffset, 0 })\n obj_side_top.setPosition({ 0, tableHeightOffset, depth2pos })\n obj_side_bot.setPosition({ 0, tableHeightOffset, -depth2pos })\n obj_leg1.setPosition({ -width2pos, tableHeightOffset, -depth2pos })\n obj_leg2.setPosition({ -width2pos, tableHeightOffset, depth2pos })\n obj_leg3.setPosition({ width2pos, tableHeightOffset, depth2pos })\n obj_leg4.setPosition({ width2pos, tableHeightOffset, -depth2pos })\n self.setPosition(obj_leg4.positionToWorld({ -22.12, 8.74, -19.16 }))\n --Only enabled when changing tableHeightOffset\n --obj_surface.setPosition({0,tableHeightOffset,0})\nend\n\n--Move hand zone, p=player reference, facts are scaling factors\nfunction moveHandZone(p, width2pos, depth2pos)\n local widthX = obj_side_rig.getPosition().x\n local depthZ = obj_side_top.getPosition().z\n for i = 1, p.getHandCount() do\n local handT = p.getHandTransform()\n local pos = handT.position\n local y = handT.rotation.y\n\n if y \u003c 45 or y \u003e 320 or y \u003e 135 and y \u003c 225 then\n if pos.z \u003e 0 then\n pos.z = pos.z + depth2pos - depthZ\n else\n pos.z = pos.z - depth2pos + depthZ\n end\n else\n if pos.x \u003e 0 then\n pos.x = pos.x + width2pos - widthX\n else\n pos.x = pos.x - width2pos + widthX\n end\n end\n\n --Only enabled when changing tableHeightOffset\n --pos.y = tableHeightOffset + 14\n\n handT.position = pos\n p.setHandTransform(handT, i)\n end\nend\n\n---Scales hand zones, p=player reference, facts are scaling factors\nfunction scaleHandZone(p, width, depth)\n local widthFact = width / obj_side_top.getScale().x\n local depthFact = depth / obj_side_lef.getScale().x\n for i = 1, p.getHandCount() do\n local handT = p.getHandTransform()\n local scale = handT.scale\n local y = handT.rotation.y\n if y \u003c 45 or y \u003e 320 or y \u003e 135 and y \u003c 225 then\n scale.x = scale.x * widthFact\n else\n scale.x = scale.x * depthFact\n end\n handT.scale = scale\n p.setHandTransform(handT, i)\n end\nend\n\n--Information gathering\n\n--Checks if a color is promoted or host\nfunction permissionCheck(color)\n if Player[color].host == true or Player[color].promoted == true then\n return true\n else\n return false\n end\nend\n\n--Locates a string saved within memory file\nfunction findInImageDataIndex(...)\n for _, str in ipairs({ ... }) do\n for i, v in ipairs(tableImageData) do\n if v.url == str or v.name == str then\n return i\n end\n end\n end\n return nil\nend\n\n--Round number (num) to the Nth decimal (dec)\nfunction round(num, dec)\n local mult = 10 ^ (dec or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n--Locates a button with a helper function\nfunction findButton(obj, func)\n if func == nil then error(\"No func supplied to findButton\") end\n for _, v in ipairs(obj.getButtons()) do\n if func(v) then\n return v\n end\n end\n return nil\nend\n\n--Creation of buttons/inputs\n\nfunction createOpenCloseButton()\n local tooltip = \"Open Table Control Panel\"\n if controlActive then\n tooltip = \"Close Table Control Panel\"\n end\n self.createButton({\n click_function = \"click_toggleControl\",\n function_owner = self,\n position = { 0, 0, 0 },\n rotation = { -45, 0, 0 },\n height = 400,\n width = 400,\n color = { 1, 1, 1, 0 },\n tooltip = tooltip\n })\nend\n\nfunction createSurfaceInput()\n local currentURL = obj_surface.getCustomObject().diffuse\n local nickname = \"\"\n if findInImageDataIndex(currentURL) ~= nil then\n nickname = tableImageData[findInImageDataIndex(currentURL)].name\n end\n self.createInput({\n label = \"Nickname\",\n input_function = \"none\",\n function_owner = self,\n alignment = 3,\n position = { 0, 0, 2 },\n height = 224,\n width = 4000,\n font_size = 200,\n tooltip = \"Enter nickname for table image (only used for save)\",\n value = nickname\n })\n self.createInput({\n label = \"URL\",\n input_function = \"none\",\n function_owner = self,\n alignment = 3,\n position = { 0, 0, 3 },\n height = 224,\n width = 4000,\n font_size = 200,\n tooltip = \"Enter URL for tabletop image\",\n value = currentURL\n })\nend\n\nfunction createSurfaceButtons()\n --Label\n self.createButton({\n label = \"Tabletop Surface Image\",\n click_function = \"none\",\n position = { 0, 0, 1 },\n height = 0,\n width = 0,\n font_size = 300,\n font_color = { 1, 1, 1 }\n })\n --Functional\n self.createButton({\n label = \"Apply Image\\nTo Table\",\n click_function = \"click_applySurface\",\n function_owner = self,\n tooltip = \"Apply URL as table image\",\n position = { 2, 0, 4 },\n height = 440,\n width = 1400,\n font_size = 200,\n })\n self.createButton({\n label = \"Save Image\\nTo Memory\",\n click_function = \"click_saveSurface\",\n function_owner = self,\n tooltip = \"Record URL into memory (requires nickname)\",\n position = { -2, 0, 4 },\n height = 440,\n width = 1400,\n font_size = 200,\n })\n --Label\n self.createButton({\n label = \"Load From Memory\",\n click_function = \"none\",\n position = { 0, 0, 5.5 },\n height = 0,\n width = 0,\n font_size = 300,\n font_color = { 1, 1, 1 }\n })\n --Saves, created dynamically from memory file\n for i, memoryEntry in ipairs(tableImageData) do\n --Load\n local funcName = i .. \"loadMemory\"\n local func = function(x, y) click_loadMemory(x, y, i) end\n self.setVar(funcName, func)\n self.createButton({\n label = memoryEntry.name,\n click_function = funcName,\n function_owner = self,\n tooltip = memoryEntry.url,\n font_size = 200,\n position = { -0.6, 0, 6.5 + 0.5 * (i - 1) },\n height = 240,\n width = 3300,\n })\n --Delete\n local funcName = i .. \"deleteMemory\"\n local func = function(x, y) click_deleteMemory(x, y, i) end\n self.setVar(funcName, func)\n self.createButton({\n label = \"DELETE\",\n click_function = funcName,\n function_owner = self,\n tooltip = \"\",\n position = { 3.6, 0, 6.5 + 0.5 * (i - 1) },\n height = 240,\n width = 600,\n font_size = 160,\n font_color = { 1, 0, 0 },\n color = { 0.8, 0.8, 0.8 }\n })\n end\nend\n\nfunction createScaleInput()\n self.createInput({\n label = string.char(8644),\n input_function = \"none\",\n function_owner = self,\n alignment = 3,\n position = { -8.5, 0, 2 },\n height = 224,\n width = 400,\n font_size = 200,\n tooltip = \"Table Width\",\n value = round(obj_side_top.getScale().x, 1)\n })\n self.createInput({\n label = string.char(8645),\n input_function = \"none\",\n function_owner = self,\n alignment = 3,\n position = { -7.5, 0, 2 },\n height = 224,\n width = 400,\n font_size = 200,\n tooltip = \"Table Depth\",\n value = round(obj_side_lef.getScale().x, 1)\n })\nend\n\nfunction createScaleButtons()\n --Labels\n self.createButton({\n label = \"Table Scale\",\n click_function = \"none\",\n position = { -8, 0, 1 },\n height = 0,\n width = 0,\n font_size = 300,\n font_color = { 1, 1, 1 }\n })\n self.createButton({\n label = string.char(8644) .. \" \" .. string.char(8645),\n click_function = \"none\",\n position = { -8, 0, 2 },\n height = 0,\n width = 0,\n font_size = 300,\n font_color = { 1, 1, 1 }\n })\n self.createButton({\n label = \"Move Hands:\",\n click_function = \"none\",\n position = { -8.3, 0, 3 },\n height = 0,\n width = 0,\n font_size = 200,\n font_color = { 1, 1, 1 }\n })\n --Disabled due to me removing the feature for technical reasons\n --[[\n self.createButton({\n label=\"Scale Hands:\", click_function=\"none\",\n position={-8.3,0,4}, height=0, width=0, font_size=200, font_color={1,1,1}\n })\n ]]\n --Checkboxes\n local label = \"\"\n if checkData.move == true then label = string.char(10008) end\n self.createButton({\n label = label,\n click_function = \"click_checkMove\",\n function_owner = self,\n tooltip = \"Check to move hands when table is rescaled\",\n position = { -6.8, 0, 3 },\n height = 224,\n width = 224,\n font_size = 200,\n })\n --[[\n local label = \"\"\n if checkData.scale == true then label = string.char(10008) end\n self.createButton({\n label=label, click_function=\"click_checkScale\",\n function_owner=self, tooltip=\"Check to scale the width of hands when table is rescaled\",\n position={-6.8,0,4}, height=224, width=224, font_size=200,\n })\n ]]\n --Apply button\n self.createButton({\n label = \"Apply Scale\",\n click_function = \"click_applyScale\",\n function_owner = self,\n tooltip = \"Apply width/depth to table\",\n position = { -8, 0, 4 },\n height = 440,\n width = 1400,\n font_size = 200,\n })\nend\n\n--Data tables\n\nref_noninteractable = {\n \"afc863\", \"c8edca\", \"393bf7\", \"12c65e\", \"f938a2\", \"9f95fd\", \"35b95f\",\n \"5af8f2\", \"4ee1f2\", \"bd69bd\"\n}\n\nref_playerColor = {\n \"White\", \"Brown\", \"Red\", \"Orange\", \"Yellow\",\n \"Green\", \"Teal\", \"Blue\", \"Purple\", \"Pink\", \"Black\"\n}\n\n--Dummy function, absorbs unwanted triggers\nfunction none() end", "LuaScriptState": "{\"cd\":{\"move\":false,\"scale\":false},\"tid\":[{\"name\":\"Felt - Grey\",\"url\":\"https://i.imgur.com/N0O6aqj.jpg\"},{\"name\":\"Wood\",\"url\":\"https://i.imgur.com/iOFFsGh.jpg\"},{\"name\":\"Wood 2\",\"url\":\"https://i.imgur.com/SQ2t01d.jpg\"}]}", "MaterialIndex": 1, "MeasureMovement": false, @@ -1375,7 +1511,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/MythosArea\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\nlocal ENCOUNTER_DECK_AREA = {\n upperLeft = { x = 0.9, z = 0.42 },\n lowerRight = { x = 0.86, z = 0.38 }\n}\nlocal ENCOUNTER_DISCARD_AREA = {\n upperLeft = { x = 1.62, z = 0.42 },\n lowerRight = { x = 1.58, z = 0.38 }\n}\n\n-- global position of encounter deck and discard pile\nlocal ENCOUNTER_DECK_POS = { x = -3.93, y = 1, z = 5.76 }\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1, z = 10.38 }\nlocal isReshuffling = false\nlocal collisionEnabled = false\nlocal currentScenario, useFrontData, tokenData\nlocal TRASH, DATA_HELPER\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedState = JSON.decode(savedData) or {}\n currentScenario = loadedState.currentScenario or \"\"\n useFrontData = loadedState.useFrontData or true\n tokenData = loadedState.tokenData or {}\n end\n TRASH = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n DATA_HELPER = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\nfunction onSave()\n return JSON.encode({\n currentScenario = currentScenario,\n useFrontData = useFrontData,\n tokenData = tokenData\n })\nend\n\n---------------------------------------------------------\n-- collison and container event handling\n---------------------------------------------------------\n\n-- TTS event handler. Handles scenario name event triggering and encounter card token resets.\nfunction onCollisionEnter(collisionInfo)\n if not collisionEnabled then return end\n\n local object = collisionInfo.collision_object\n\n -- early exit for better performance\n if object.type ~= \"Card\" then return end\n\n -- get scenario name and maybe fire followup event\n if object.getName() == \"Scenario\" then\n local description = object.getDescription()\n\n -- detect if a new scenario card is placed down\n if currentScenario ~= description then\n currentScenario = description\n fireScenarioChangedEvent()\n end\n\n local metadata = JSON.decode(object.getGMNotes()) or {}\n if not metadata[\"tokens\"] then\n tokenData = {}\n return\n end\n\n -- detect orientation of scenario card (for difficulty)\n useFrontData = not object.is_face_down\n tokenData = metadata[\"tokens\"][(useFrontData and \"front\" or \"back\")]\n fireTokenDataChangedEvent()\n end\n\n local localPos = self.positionToLocal(object.getPosition())\n if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then\n Wait.frames(function() tokenSpawnTrackerApi.resetTokensSpawned(object.getGUID()) end, 1)\n removeTokensFromObject(object)\n end\nend\n\n-- TTS event handler. Handles scenario name event triggering\nfunction onCollisionExit(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- reset token metadata if scenario reference card is removed\n if object.getName() == \"Scenario\" then\n tokenData = {}\n useFrontData = nil\n fireTokenDataChangedEvent()\n end\nend\n\n-- Listens for cards entering the encounter deck or encounter discard, discards tokens on them,\n-- and resets the spawn state for the cards when they do.\nfunction onObjectEnterContainer(container, object)\n local localPos = self.positionToLocal(container.getPosition())\n if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object.getGUID())\n removeTokensFromObject(object)\n end\nend\n\n-- fires if the scenario title changes\nfunction fireScenarioChangedEvent()\n -- maybe show the title splash screen\n Wait.frames(function() Global.call('titleSplash', currentScenario) end, 20)\n\n -- set the scenario for the playarea (connections might be disabled)\n playAreaApi.onScenarioChanged(currentScenario)\n\n -- maybe update the playarea image\n local playAreaImageSelector = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaImageSelector\")\n playAreaImageSelector.call(\"maybeUpdatePlayAreaImage\", currentScenario)\nend\n\n-- fires if the scenario title or the difficulty changes\nfunction fireTokenDataChangedEvent()\n local fullData = returnTokenData()\n tokenArrangerApi.onTokenDataChanged(fullData)\nend\n\n-- returns the chaos token metadata (if provided)\nfunction returnTokenData()\n return {\n tokenData = tokenData,\n currentScenario = currentScenario,\n useFrontData = useFrontData\n }\nend\n\n---------------------------------------------------------\n-- encounter card drawing\n---------------------------------------------------------\n\n-- gets the encounter deck (for internal functions and Api calls)\nfunction getEncounterDeck()\n local searchResult = searchLib.atPosition(ENCOUNTER_DECK_POS, \"isCardOrDeck\")\n\n if #searchResult \u003e 0 then\n return searchResult[1]\n end\nend\n\n-- 'params' contains the position, rotation and a boolean to force a faceup draw\nfunction drawEncounterCard(params)\n local encounterDeck = getEncounterDeck()\n local reshuffledAlready\n\n if encounterDeck then\n reshuffledAlready = false\n if encounterDeck.type == \"Deck\" then\n actualEncounterCardDraw(encounterDeck.takeObject(), params)\n else\n actualEncounterCardDraw(encounterDeck, params)\n end\n else\n -- nothing here, time to reshuffle\n if reshuffledAlready == true then\n reshuffledAlready = false\n return\n end\n\n -- if there is no discard pile either, reshuffleEncounterDeck will give an error message already\n reshuffleEncounterDeck()\n reshuffledAlready = true\n drawEncounterCard(params)\n end\nend\n\n-- draw the provided card to the requesting playmat\nfunction actualEncounterCardDraw(card, params)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n\n -- draw hidden cards facedown\n local faceUpRotation = 0\n if metadata.hidden or DATA_HELPER.call('checkHiddenCard', card.getName()) then\n faceUpRotation = 180\n end\n\n local rot = playmatApi.returnRotation(params.matColor):setAt(\"z\", faceUpRotation)\n deckLib.placeOrMergeIntoDeck(card, params.position, rot)\nend\n\nfunction reshuffleEncounterDeck()\n -- flag to avoid multiple calls\n if isReshuffling then return end\n isReshuffling = true\n\n local encounterDeck = getEncounterDeck()\n local discardPile = searchLib.atPosition(ENCOUNTER_DISCARD_POSITION, \"isCardOrDeck\")\n\n if #discardPile \u003e 0 then\n local discardDeck = discardPile[1]\n\n -- flips discard pile\n if not discardDeck.is_face_down then\n discardDeck.setRotation({ 0, -90, 180 })\n end\n\n -- make a new encounter deck\n if encounterDeck == nil then\n discardDeck.setPosition(Vector(ENCOUNTER_DECK_POS) + Vector({ 0, 1, 0 }))\n encounterDeck = discardDeck\n else\n encounterDeck.putObject(discardDeck)\n end\n encounterDeck.shuffle()\n broadcastToAll(\"Shuffled encounter discard into deck.\", \"White\")\n else\n broadcastToAll(\"Encounter discard pile is already empty.\", \"Red\")\n end\n\n -- disable flag\n Wait.time(function() isReshuffling = false end, 1)\nend\n\n---------------------------------------------------------\n-- helper functions\n---------------------------------------------------------\n\n-- Simple method to check if the given point is in a specified area\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n for _, obj in ipairs(searchLib.onObject(object)) do\n if obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false and\n not tokenChecker.isChaosToken(obj) then\n TRASH.putObject(obj)\n end\n end\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/MythosArea\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/MythosArea\")\nend)\n__bundle_register(\"core/MythosArea\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\nlocal ENCOUNTER_DECK_AREA = {\n upperLeft = { x = 1.05, z = 0.15 },\n lowerRight = { x = 0.70, z = 0.59 }\n}\nlocal ENCOUNTER_DISCARD_AREA = {\n upperLeft = { x = 1.77, z = 0.15 },\n lowerRight = { x = 1.42, z = 0.59 }\n}\n\n-- global position of encounter deck and discard pile\nlocal ENCOUNTER_DECK_POS = { x = -3.93, y = 1, z = 5.76 }\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1, z = 10.38 }\nlocal isReshuffling = false\nlocal collisionEnabled = false\nlocal currentScenario, useFrontData, tokenData\n\nfunction onSave()\n return JSON.encode({\n currentScenario = currentScenario,\n useFrontData = useFrontData,\n tokenData = tokenData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedState = JSON.decode(savedData) or {}\n currentScenario = loadedState.currentScenario or \"\"\n useFrontData = loadedState.useFrontData or true\n tokenData = loadedState.tokenData or {}\n end\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- collison and container event handling\n---------------------------------------------------------\n\n-- TTS event handler. Handles scenario name event triggering and encounter card token resets.\nfunction onCollisionEnter(collisionInfo)\n if not collisionEnabled then return end\n\n local object = collisionInfo.collision_object\n\n -- early exit for better performance\n if object.type ~= \"Card\" then return end\n\n -- get scenario name and maybe fire followup event\n if object.getName() == \"Scenario\" then\n local description = object.getDescription()\n\n -- detect if a new scenario card is placed down\n if currentScenario ~= description then\n currentScenario = description\n fireScenarioChangedEvent()\n end\n\n local metadata = JSON.decode(object.getGMNotes()) or {}\n if not metadata[\"tokens\"] then\n tokenData = {}\n return\n end\n\n -- detect orientation of scenario card (for difficulty)\n useFrontData = not object.is_face_down\n tokenData = metadata[\"tokens\"][(useFrontData and \"front\" or \"back\")]\n fireTokenDataChangedEvent()\n end\n\n local localPos = self.positionToLocal(object.getPosition())\n if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then\n Wait.frames(function() tokenSpawnTrackerApi.resetTokensSpawned(object) end, 1)\n removeTokensFromObject(object)\n end\nend\n\n-- TTS event handler. Handles scenario name event triggering\nfunction onCollisionExit(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- reset token metadata if scenario reference card is removed\n if object.getName() == \"Scenario\" then\n tokenData = {}\n useFrontData = nil\n fireTokenDataChangedEvent()\n end\nend\n\n-- Listens for cards entering the encounter deck or encounter discard, discards tokens on them,\n-- and resets the spawn state for the cards when they do.\nfunction onObjectEnterContainer(container, object)\n local localPos = self.positionToLocal(container.getPosition())\n if inArea(localPos, ENCOUNTER_DECK_AREA) or inArea(localPos, ENCOUNTER_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- fires if the scenario title changes\nfunction fireScenarioChangedEvent()\n -- maybe show the title splash screen\n Wait.frames(function() Global.call('titleSplash', currentScenario) end, 20)\n\n -- set the scenario for the playarea (connections might be disabled)\n playAreaApi.onScenarioChanged(currentScenario)\n\n -- maybe update the playarea image\n local playAreaImageSelector = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaImageSelector\")\n playAreaImageSelector.call(\"maybeUpdatePlayAreaImage\", currentScenario)\nend\n\n-- fires if the scenario title or the difficulty changes\nfunction fireTokenDataChangedEvent()\n local fullData = returnTokenData()\n tokenArrangerApi.onTokenDataChanged(fullData)\nend\n\n-- returns the chaos token metadata (if provided)\nfunction returnTokenData()\n return {\n tokenData = tokenData,\n currentScenario = currentScenario,\n useFrontData = useFrontData\n }\nend\n\n---------------------------------------------------------\n-- encounter card drawing\n---------------------------------------------------------\n\n-- gets the encounter deck (for internal functions and Api calls)\nfunction getEncounterDeck()\n local searchResult = searchLib.atPosition(ENCOUNTER_DECK_POS, \"isCardOrDeck\")\n\n if #searchResult \u003e 0 then\n return searchResult[1]\n end\nend\n\n-- 'params' contains the position, rotation and a boolean to force a faceup draw\nfunction drawEncounterCard(params)\n local encounterDeck = getEncounterDeck()\n local reshuffledAlready\n\n if encounterDeck then\n reshuffledAlready = false\n if encounterDeck.type == \"Deck\" then\n actualEncounterCardDraw(encounterDeck.takeObject(), params)\n else\n actualEncounterCardDraw(encounterDeck, params)\n end\n else\n -- nothing here, time to reshuffle\n if reshuffledAlready == true then\n reshuffledAlready = false\n return\n end\n\n -- if there is no discard pile either, reshuffleEncounterDeck will give an error message already\n local success = reshuffleEncounterDeck()\n\n -- only continue if there was a discard pile\n if not success then return end\n reshuffledAlready = true\n drawEncounterCard(params)\n end\nend\n\n-- draw the provided card to the requesting playermat\nfunction actualEncounterCardDraw(card, params)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n local DATA_HELPER = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n\n -- draw hidden cards facedown\n local faceUpRotation = 0\n if metadata.hidden or DATA_HELPER.call('checkHiddenCard', card.getName()) then\n faceUpRotation = 180\n end\n\n local rot = playermatApi.returnRotation(params.matColor):setAt(\"z\", faceUpRotation)\n deckLib.placeOrMergeIntoDeck(card, params.position, rot)\nend\n\n-- gets the discard pile and shuffles it into the encounter deck\n---@return boolean: Whether the operation was successfully performed\nfunction reshuffleEncounterDeck()\n -- flag to avoid multiple calls\n if isReshuffling then\n return false\n end\n isReshuffling = true\n\n -- disable flag after 1s delay\n Wait.time(function() isReshuffling = false end, 1)\n\n local encounterDeck = getEncounterDeck()\n local discardPile = searchLib.atPosition(ENCOUNTER_DISCARD_POSITION, \"isCardOrDeck\")\n\n if #discardPile \u003e 0 then\n local discardDeck = discardPile[1]\n\n -- flips discard pile\n if not discardDeck.is_face_down then\n discardDeck.setRotation({ 0, -90, 180 })\n end\n\n -- make a new encounter deck\n if encounterDeck == nil then\n discardDeck.setPosition(Vector(ENCOUNTER_DECK_POS) + Vector({ 0, 1, 0 }))\n encounterDeck = discardDeck\n else\n encounterDeck.putObject(discardDeck)\n end\n encounterDeck.shuffle()\n broadcastToAll(\"Shuffled encounter discard into deck.\", \"White\")\n return true\n else\n broadcastToAll(\"Encounter discard pile is already empty.\", \"Red\")\n return false\n end\nend\n\n---------------------------------------------------------\n-- helper functions\n---------------------------------------------------------\n\n-- Simple method to check if the given point is in a specified area\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003e bounds.upperLeft.z\n and point.z \u003c bounds.lowerRight.z)\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n local TRASH = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n for _, obj in ipairs(searchLib.onObject(object, \"isTileOrToken\")) do\n if obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.memo ~= nil and\n obj.getLock() == false and\n not tokenChecker.isChaosToken(obj) then\n TRASH.putObject(obj)\n end\n end\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"currentScenario\":\"\",\"tokenData\":[],\"useFrontData\":true}", "MeasureMovement": false, "Name": "Custom_Tile", @@ -2095,7 +2231,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DoomCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal optionsVisible = false\nlocal options = {\n Agenda = true,\n Playarea = true,\n Playermats = true\n}\n\nval = 0\n\n-- save current value and options\nfunction onSave() return JSON.encode({ val, options }) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n val = loadedData[1]\n options = loadedData[2]\n\n -- restore state for option panel\n for key, bool in pairs(options) do\n self.UI.setAttribute(\"option\" .. key, \"isOn\", not bool)\n end\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = { 0, 0.06, 0 },\n height = 800,\n width = 800,\n font_size = 650,\n scale = { 1.5, 1.5, 1.5 },\n font_color = { 1, 1, 1, 95 },\n color = { 0, 0, 0, 0 }\n })\nend\n\n-- called by the invisible button to change displayed value\nfunction addOrSubtract(_, _, isRightClick)\n local newVal = math.min(math.max(val + (isRightClick and -1 or 1), 0), 99)\n if val ~= newVal then\n updateVal(newVal)\n end\nend\n\n-- adds the provided number to the current count\nfunction addVal(number)\n val = val + number\n updateVal(val)\nend\n\n-- sets the current count to the provided number\nfunction updateVal(number)\n val = number or 0\n self.editButton({ index = 0, label = tostring(val) })\n if number then\n broadcastDoom(val)\n else\n broadcastToAll(\"0 doom on the agenda\", \"White\")\n end\nend\n\n-- called by updateVal and addVal to broadcast total doom in play, including doom threshold\nfunction broadcastDoom(val)\n local doomInPlayCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomInPlayCounter\")\n local doomInPlay = doomInPlayCounter.call(\"countDoomInPlay\") + val\n local doomThreshold = getDoomThreshold()\n\n if doomThreshold then\n broadcastToAll(val .. \" doom on the agenda (\" .. doomInPlay .. \"/\" .. doomThreshold .. \" in play)\", \"White\")\n else\n broadcastToAll(val .. \" doom on the agenda (\" .. doomInPlay .. \" in play)\", \"White\")\n end\nend\n\n-- called by \"Reset\" button to remove doom\nfunction startReset()\n if options.Agenda then\n -- omitting the number will broadcast a special message just for this case\n updateVal()\n end\n local doomInPlayCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomInPlayCounter\")\n if doomInPlayCounter then\n doomInPlayCounter.call(\"removeDoom\", options)\n end\nend\n\n-- get doom threshold from top card of Agenda deck\nfunction getDoomThreshold()\n local agendaPos = { -2.72, 1.6, 0.37 }\n local searchResult = searchLib.atPosition(agendaPos, \"isCardOrDeck\")\n\n if #searchResult == 1 then\n local obj = searchResult[1]\n if obj.type == \"Card\" then\n return getDoomThresholdFromGMNotes(obj.getGMNotes())\n else\n -- handle agenda deck\n local containedObjects = obj.getData().ContainedObjects\n local topCardData = containedObjects[#containedObjects]\n return getDoomThresholdFromGMNotes(topCardData.GMNotes)\n end\n end\n return nil\nend\n\n-- decodes the gm notes and return the doom treshhold\nfunction getDoomThresholdFromGMNotes(notes)\n local metadata = JSON.decode(notes) or {}\n if metadata.doomThresholdPerInvestigator then\n return metadata.doomThresholdPerInvestigator * playAreaApi.getInvestigatorCount() + metadata.doomThreshold\n else\n return metadata.doomThreshold\n end\nend\n\n-- XML UI functions\nfunction optionClick(_, optionName)\n options[optionName] = not options[optionName]\n printToAll(\"Doom removal of \" .. optionName .. (options[optionName] and \" enabled\" or \" disabled\"))\nend\n\nfunction toggleOptions()\n optionsVisible = not optionsVisible\n\n if optionsVisible then\n self.UI.show(\"Options\")\n else\n self.UI.hide(\"Options\")\n end\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DoomCounter\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DoomCounter\")\nend)\n__bundle_register(\"core/DoomCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal optionsVisible = false\nlocal options = {\n Agenda = true,\n Playarea = true,\n Playermats = true\n}\n\nval = 0\n\n-- save current value and options\nfunction onSave() return JSON.encode({ val, options }) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n val = loadedData[1]\n options = loadedData[2]\n\n -- restore state for option panel\n for key, bool in pairs(options) do\n self.UI.setAttribute(\"option\" .. key, \"isOn\", not bool)\n end\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = { 0, 0.06, 0 },\n height = 800,\n width = 800,\n font_size = 650,\n scale = { 1.5, 1.5, 1.5 },\n font_color = { 1, 1, 1, 95 },\n color = { 0, 0, 0, 0 }\n })\nend\n\n-- called by the invisible button to change displayed value\nfunction addOrSubtract(_, _, isRightClick)\n local newVal = math.min(math.max(val + (isRightClick and -1 or 1), 0), 99)\n if val ~= newVal then\n updateVal(newVal)\n end\nend\n\n-- adds the provided number to the current count\nfunction addVal(number)\n val = val + number\n updateVal(val)\nend\n\n-- sets the current count to the provided number\nfunction updateVal(number)\n val = number or 0\n self.editButton({ index = 0, label = tostring(val) })\n if number then\n broadcastDoom(val)\n else\n broadcastToAll(\"0 doom on the agenda\", \"White\")\n end\nend\n\n-- called by updateVal and addVal to broadcast total doom in play, including doom threshold\nfunction broadcastDoom(val)\n local doomInPlayCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomInPlayCounter\")\n local doomInPlay = doomInPlayCounter.call(\"countDoomInPlay\") + val\n local doomThreshold = getDoomThreshold()\n\n if doomThreshold then\n broadcastToAll(val .. \" doom on the agenda (\" .. doomInPlay .. \"/\" .. doomThreshold .. \" in play)\", \"White\")\n else\n broadcastToAll(val .. \" doom on the agenda (\" .. doomInPlay .. \" in play)\", \"White\")\n end\nend\n\n-- called by \"Reset\" button to remove doom\nfunction startReset()\n if options.Agenda then\n -- omitting the number will broadcast a special message just for this case\n updateVal()\n end\n local doomInPlayCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomInPlayCounter\")\n if doomInPlayCounter then\n doomInPlayCounter.call(\"removeDoom\", options)\n end\nend\n\n-- get doom threshold from top card of Agenda deck\nfunction getDoomThreshold()\n local agendaPos = { -2.72, 1.6, 0.37 }\n local searchResult = searchLib.atPosition(agendaPos, \"isCardOrDeck\")\n\n if #searchResult == 1 then\n local obj = searchResult[1]\n if obj.type == \"Card\" then\n return getDoomThresholdFromGMNotes(obj.getGMNotes())\n else\n -- handle agenda deck\n local containedObjects = obj.getData().ContainedObjects\n local topCardData = containedObjects[#containedObjects]\n return getDoomThresholdFromGMNotes(topCardData.GMNotes)\n end\n end\n return nil\nend\n\n-- decodes the gm notes and return the doom treshhold\nfunction getDoomThresholdFromGMNotes(notes)\n local metadata = JSON.decode(notes) or {}\n if metadata.doomThresholdPerInvestigator then\n return metadata.doomThresholdPerInvestigator * playAreaApi.getInvestigatorCount() + metadata.doomThreshold\n else\n return metadata.doomThreshold\n end\nend\n\n-- XML UI functions\nfunction optionClick(_, optionName)\n options[optionName] = not options[optionName]\n printToAll(\"Doom removal of \" .. optionName .. (options[optionName] and \" enabled\" or \" disabled\"))\nend\n\nfunction toggleOptions()\n optionsVisible = not optionsVisible\n\n if optionsVisible then\n self.UI.show(\"Options\")\n else\n self.UI.hide(\"Options\")\n end\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[0,{\"Agenda\":true,\"Playarea\":true,\"Playermats\":true}]", "MeasureMovement": false, "Name": "Custom_Token", @@ -2269,7 +2405,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/ActiveInvestigatorCounter\")\nend)\n__bundle_register(\"core/ActiveInvestigatorCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nMIN_VALUE = 1\nMAX_VALUE = 4\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/ActiveInvestigatorCounter\")\nend)\n__bundle_register(\"core/ActiveInvestigatorCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nMIN_VALUE = 1\nMAX_VALUE = 4\nend)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "2", "MeasureMovement": false, "Name": "Custom_Token", @@ -2305,9 +2441,9 @@ "Autoraise": true, "CardID": 265902, "ColorDiffuse": { - "b": 0.71324, - "g": 0.71324, - "r": 0.71324 + "b": 0, + "g": 0, + "r": 0 }, "CustomDeck": { "2659": { @@ -2326,7 +2462,7 @@ "GUID": "68fe54", "Grid": true, "GridProjection": false, - "Hands": true, + "Hands": false, "HideWhenFaceDown": false, "IgnoreFoW": false, "LayoutGroupSortIndex": 0, @@ -2384,7 +2520,7 @@ "GUID": "91c83e", "Grid": true, "GridProjection": false, - "Hands": true, + "Hands": false, "HideWhenFaceDown": false, "IgnoreFoW": false, "LayoutGroupSortIndex": 0, @@ -2627,7 +2763,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "0", "MeasureMovement": false, "Name": "Custom_Token", @@ -2687,7 +2823,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "5", "MeasureMovement": false, "Name": "Custom_Token", @@ -3314,7 +3450,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "5", "MeasureMovement": false, "Name": "Custom_Token", @@ -3425,12 +3561,12 @@ "CustomShader": { "FresnelStrength": 0.1, "SpecularColor": { - "b": 0.745098054, + "b": 0.7450981, "g": 0.8117647, "r": 0.8745098 }, "SpecularIntensity": 0.05, - "SpecularSharpness": 3.60000014 + "SpecularSharpness": 3.6 }, "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/863978359495062918/777CFB72754EC943DF113C1EC1CA495B07FCB6C1/", "MaterialIndex": 1, @@ -3458,9 +3594,9 @@ "Sticky": false, "Tooltip": true, "Transform": { - "posX": -66.409, + "posX": -66.41, "posY": 1.863, - "posZ": -84.545, + "posZ": -84.55, "rotX": 90, "rotY": 151, "rotZ": 0, @@ -8121,12 +8257,125 @@ "y": 0, "z": 0 }, + "AttachedDecals": [ + { + "CustomDecal": { + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2501268517218943111/803E57A7B3E9765DF342050EE6C71D69473A7388/", + "Name": "Image #1", + "Size": 1 + }, + "Transform": { + "posX": -0.93, + "posY": 0.105, + "posZ": 0.66, + "rotX": 90, + "rotY": 180, + "rotZ": 0, + "scaleX": 0.6, + "scaleY": 0.6, + "scaleZ": 1 + } + }, + { + "CustomDecal": { + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037357792052848566/5DA900C430E97D3DFF2C9B8A3DB1CB2271791FC7/", + "Name": "Image #2", + "Size": 1 + }, + "Transform": { + "posX": -1.05, + "posY": 0.105, + "posZ": -0.567, + "rotX": 90, + "rotY": 205, + "rotZ": 0, + "scaleX": 0.3, + "scaleY": 0.3, + "scaleZ": 1 + } + }, + { + "CustomDecal": { + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2501268517219098388/0936FEE03B410319658B5E05DB5D486CEDDE98F5/", + "Name": "Image #3", + "Size": 1 + }, + "Transform": { + "posX": 0, + "posY": 0.105, + "posZ": -0.81, + "rotX": 90, + "rotY": 180, + "rotZ": 0, + "scaleX": 2.4, + "scaleY": 0.009, + "scaleZ": 1 + } + } + ], "Autoraise": true, "ColorDiffuse": { "b": 1, "g": 1, "r": 1 }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 0 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "http://sfwallpaper.com/images/parchment-paper-wallpaper-10.jpg", + "ImageURL": "http://sfwallpaper.com/images/parchment-paper-wallpaper-10.jpg", + "WidthScale": 0 + }, + "Description": "", + "DragSelectable": true, + "GMNotes": "", + "GUID": "f47225", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "---@diagnostic disable\nfunction onload(saved_data)\n sheetLocked = self.script_state.sheetLocked or false\n local inverseScale = { x = math.floor(100 / self.getScale().x) / 100, y = math.floor(100 / self.getScale().z) / 100 }\n scale = self.script_state.scale or inverseScale\n flip = self.script_state.flip or \"False\"\n fields = self.script_state.fields or {}\n checks = self.script_state.checks or {}\n decals = self.script_state.decals or {}\n height = self.script_state.height or 0.5\n locks = self.script_state.locks or { fields = false, checks = false, decals = false }\n lookupInputIndexToInfo = {}\n lookupButtonIndexToInfo = {}\n lookupFieldIndices = {}\n lookupCheckIndices = {}\n lookupDecalIndices = {}\n lookupSelectionButtonIndices = {}\n lastFieldLockedMessage = 0\n buttonIndex = 0\n inputIndex = 0\n if saved_data ~= \"\" then\n local loadedData = JSON.decode(saved_data)\n sheetLocked = loadedData.sheetLocked or false\n flip = loadedData.flip or \"False\"\n height = loadedData.height or 0.5\n fields = loadedData.fields or {}\n checks = loadedData.checks or {}\n decals = loadedData.decals or {}\n scale = loadedData.scale or inverseScale\n locks = loadedData.locks or { fields = false, checks = false, decals = false }\n end\n if (not getCommited()) then\n self.addContextMenuItem(\"Edit Layout\", showEditPanel)\n self.addContextMenuItem(\"Commit Layout\", showCommitPanel)\n nudgeDistance = self.script_state.nudgeDistance or 0.1\n page = 1\n editingSheet = false\n selectedId = 0\n selectedArrayId = 1\n selectedType = \"\"\n selectedMax = 5\n creating = false\n else\n makeContextMenuItems()\n end\n createAll()\nend\n\nfunction makeContextMenuItems()\n self.clearContextMenu()\n if (not sheetLocked) then\n local getLocked = function(element)\n if (locks[element]) then return \"◆ Lock\" else return \"◇ Lock\" end\n end\n local allLocked = \"Lock\"\n if (locks.fields and locks.checks and locks.decals) then allLocked = \"Unlock\" end\n if (#fields \u003e 0 and #checks \u003e 0 and #decals \u003e 0) then\n self.addContextMenuItem(allLocked .. \" everything\", toggleAllLocks)\n end\n for k, v in pairs(fields) do\n if (v.locked ~= \"True\") then\n self.addContextMenuItem(getLocked(\"fields\") .. \" texts\", toggleLockFields)\n break\n end\n end\n for k, v in pairs(checks) do\n if (v.locked ~= \"True\") then\n self.addContextMenuItem(getLocked(\"checks\") .. \" checkboxes\", toggleLockChecks)\n break\n end\n end\n for k, v in pairs(decals) do\n if (v.locked ~= \"True\") then\n self.addContextMenuItem(getLocked(\"decals\") .. \" images\", toggleLockDecals)\n break\n end\n end\n end\nend\n\nfunction toggleAllLocks(ply, pos, obj)\n if (locks.fields and locks.checks and locks.decals) then\n locks.fields = false\n locks.checks = false\n locks.decals = false\n broadcastToColor(\"Unlocked everything\", ply)\n else\n locks.fields = true\n locks.checks = true\n locks.decals = true\n broadcastToColor(\"Locked everything\", ply)\n end\n updateSave()\n makeContextMenuItems()\n refresh()\nend\n\nfunction toggleLockFields(ply, pos, obj)\n if (locks.fields) then\n locks.fields = false\n broadcastToColor(\"Unlocked texts\", ply)\n else\n locks.fields = true\n broadcastToColor(\"Locked texts\", ply)\n end\n updateSave()\n makeContextMenuItems()\n refresh()\nend\n\nfunction toggleLockChecks(ply, pos, obj)\n if (locks.checks) then\n locks.checks = false\n broadcastToColor(\"Unlocked checkboxes\", ply)\n else\n locks.checks = true\n broadcastToColor(\"Locked checkboxes\", ply)\n end\n updateSave()\n makeContextMenuItems()\n refresh()\nend\n\nfunction toggleLockDecals(ply, pos, obj)\n if (locks.decals) then\n locks.decals = false\n broadcastToColor(\"Unlocked images\", ply)\n else\n locks.decals = true\n broadcastToColor(\"Locked images\", ply)\n end\n updateSave()\n makeContextMenuItems()\n refresh()\nend\n\nfunction updateSave()\n local data_to_save = {\n scale = scale,\n height = height,\n fields = fields,\n checks = checks,\n decals = decals,\n flip = flip,\n sheetLocked = sheetLocked,\n locks = locks\n }\n if (not getCommited()) then\n data_to_save.nudgeDistance = nudgeDistance\n else\n data_to_save.nudgeDistance = nil\n end\n saved_data = JSON.encode(data_to_save)\n self.script_state = saved_data\nend\n\nfunction null() end\n\nfunction refresh()\n if (not creating) then\n self.clearInputs()\n self.clearButtons()\n inputIndex = 0\n buttonIndex = 0\n if (editingSheet) then\n createSelectionHighlight()\n end\n createAll()\n end\nend\n\nfunction createAll()\n lookupInputIndexToInfo = {}\n lookupButtonIndexToInfo = {}\n lookupFieldIndices = {}\n lookupCheckIndices = {}\n lookupDecalIndices = {}\n startLuaCoroutine(self, \"createAllCoroutine\")\nend\n\nfunction createAllCoroutine()\n if (not getCommited()) then\n UI.setAttribute(getPanelId(\"Loading\"), \"active\", \"True\")\n end\n coroutine.yield(0)\n creating = true\n for fieldID, field in pairs(fields) do\n lookupFieldIndices[fieldID] = {\n inputs = {},\n totals = {},\n counterButtons = {},\n selectionButtons = {}\n }\n local posx = field.pos.x\n local posy = field.pos.y\n local func = \"edit\"\n if (field.vsum == \"True\") then\n func = 'MarumEditableRecalculateSum_' .. fieldID\n _G[func] = function(obj, ply, input_value, selected)\n local fID = fieldID\n edit(obj, ply, input_value, selected)\n obj.call(\"recalculateVSums\", fID)\n end\n else\n local iIndex = inputIndex\n if (field.locked == \"True\" or sheetLocked or locks.fields) then\n func = 'MarumEditableRevert_' .. fieldID\n _G[func] = function(obj, ply, input_value, selected)\n local fID = fieldID\n local iID = iIndex\n local sel = selected\n obj.call(\"revertField\", { fID, ply, iID, sel })\n end\n end\n end\n\n local fieldScale = { x = scale.x, y = 1, z = scale.y }\n local rotation = { x = 0, y = 0, z = 0 }\n local posMulx = 1\n local flipped = 1\n if (flip == \"True\") then\n rotation.y = 180\n flipped = -1\n end\n local upright = self.getTransformUp().y \u003e 0\n local shouldFlip = (field.locked == \"True\" or sheetLocked or locks.fields) and upright\n local unlockedRotation = { x = rotation.x, y = rotation.y, z = rotation.z }\n if (editingSheet or shouldFlip) then\n rotation.z = 180\n posMulx = -posMulx\n fieldScale.x = -fieldScale.x\n end\n local vsum = 0\n local fontSize = field.font\n fontSize = math.min(field.size.y - 24, fontSize)\n local color = getFieldTextColor(fieldID)\n for x = 1, field.array.x do\n vsum = 0\n for y = 1, field.array.y do\n local arrayID = x + (y - 1) * field.array.x\n local pos = getFieldPosition(fieldID, x, y)\n self.createInput({\n value = field.value[arrayID],\n tooltip = getFieldTooltip(fieldID, arrayID),\n input_function = func,\n function_owner = self,\n alignment = field.align,\n position = pos,\n width = field.size.x,\n height = field.size.y,\n rotation = rotation,\n font_size = fontSize,\n scale = fieldScale,\n font_color = color,\n color = field.fieldColor,\n tab = 2\n })\n if (field.counter == \"True\") then\n local counterButtonWidth = fontSize * 0.75\n _G['MarumEditableCounterIncrease_' .. fieldID .. '_' .. arrayID] = function(obj, ply, alt)\n local fID = fieldID\n local aID = arrayID\n obj.call(\"increaseCounter\", { fID, aID, ply })\n end\n buttonIndex = buttonIndex + 1\n lookupButtonIndexToInfo[buttonIndex] = { type = \"counter\", id = fieldID }\n table.insert(lookupFieldIndices[fieldID].counterButtons, { index = buttonIndex, x = x, y = y, side = 1 })\n self.createButton({\n click_function = 'MarumEditableCounterIncrease_' .. fieldID .. '_' .. arrayID,\n tooltip = \"↑ Increase ↑\",\n function_owner = self,\n label = \"[b]+[/b]\",\n position = { x = pos.x + (field.size.x + counterButtonWidth) / 1000 * fieldScale.x * posMulx * flipped, y = pos.y, z = pos.z },\n rotation = unlockedRotation,\n scale = fieldScale,\n width = counterButtonWidth,\n height = counterButtonWidth,\n font_size = fontSize / 2,\n font_color = color,\n color = field.fieldColor,\n })\n\n _G['MarumEditableCounterDecrease_' .. fieldID .. '_' .. arrayID] = function(obj, ply, alt)\n local fID = fieldID\n local aID = arrayID\n obj.call(\"decreaseCounter\", { fID, aID, ply })\n end\n buttonIndex = buttonIndex + 1\n table.insert(lookupFieldIndices[fieldID].counterButtons, { index = buttonIndex, x = x, y = y, side = -1 })\n self.createButton({\n click_function = 'MarumEditableCounterDecrease_' .. fieldID .. '_' .. arrayID,\n tooltip = \"↓ Decrease ↓\",\n function_owner = self,\n label = \"[b]-[/b]\",\n position = { x = pos.x - (field.size.x + counterButtonWidth) / 1000 * fieldScale.x * posMulx * flipped, y = pos.y, z = pos.z },\n unlockedRotation = rotation,\n scale = fieldScale,\n width = counterButtonWidth,\n height = counterButtonWidth,\n font_size = fontSize / 2,\n font_color = color,\n color = field.fieldColor,\n })\n end\n inputIndex = inputIndex + 1\n lookupInputIndexToInfo[inputIndex] = { type = \"field\", id = fieldID, arrayID = arrayID }\n table.insert(lookupFieldIndices[fieldID].inputs, { index = inputIndex, arrayID = arrayID, x = x, y = y })\n if (field.vsum == \"True\") then\n if (tonumber(field.value[arrayID])) then\n vsum = vsum + tonumber(field.value[arrayID])\n end\n end\n\n if (inputIndex % 10 == 0) then coroutine.yield(0) end\n end\n\n if (field.vsum == \"True\") then\n local pos = getFieldPosition(fieldID, x, field.array.y + 1)\n local func = 'MarumEditableRevertSum_' .. fieldID\n _G[func] = function(obj, ply, input_value, selected)\n obj.call(\"revertFieldSum\", { fieldID, ply, iIndex, selected })\n end\n self.createInput({\n value = \"[u]\" .. vsum .. \"[/u]\",\n tooltip = \"[Sum]\",\n input_function = func,\n function_owner = self,\n alignment = field.align,\n position = pos,\n width = field.size.x,\n height = field.size.y,\n rotation = { x = unlockedRotation.x, y = unlockedRotation.y, z = unlockedRotation.z + 180 },\n font_size = fontSize,\n scale = { x = -scale.x, y = 1, z = scale.y },\n font_color = color,\n color = field.fieldColor,\n tab = 0\n })\n inputIndex = inputIndex + 1\n table.insert(lookupFieldIndices[fieldID].totals, { index = inputIndex, x = x })\n end\n end\n end\n\n createDecals()\n for decalID, decal in pairs(decals) do\n lookupDecalIndices[decalID] = {\n inputs = {},\n selectionButtons = {}\n }\n local pos = getDecalPosition(decalID)\n if (decal.locked ~= \"True\" and not sheetLocked and not locks.decals and not editingSheet) then\n local func = 'MarumEditableSetURL_' .. decalID\n _G[func] = function(obj, ply, alt)\n obj.call(\"showImageURLPanel\", { decalID, ply })\n end\n local tooltip = getDecalTooltip(decalID)\n self.createButton({\n value = decal.url,\n tooltip = tooltip,\n click_function = func,\n function_owner = self,\n position = pos,\n width = 490,\n height = 490,\n rotation = { x = 0, y = 0, z = 0 },\n font_size = 10,\n scale = { x = decal.scale.x * scale.x, y = decal.scale.y * scale.y, z = decal.scale.y * scale.y },\n font_color = { r = 0, g = 0, b = 0, a = 0 },\n color = { r = 0, g = 0, b = 0, a = 0 },\n })\n buttonIndex = buttonIndex + 1\n lookupButtonIndexToInfo[buttonIndex] = { type = \"decal\", id = decalID }\n table.insert(lookupDecalIndices[decalID].inputs, { index = buttonIndex })\n end\n\n if (inputIndex % 10 == 0) then coroutine.yield(0) end\n end\n\n for checkID, check in pairs(checks) do\n lookupCheckIndices[checkID] = {\n buttons = {},\n selectionButtons = {}\n }\n local rotationZ = 0\n local checkScale = { x = scale.x * check.size.x, y = 1, z = scale.y * check.size.y }\n local upright = self.getTransformUp().y \u003e 0\n local shouldFlip = (check.locked == \"True\" or sheetLocked or locks.checks) and upright\n if (editingSheet or shouldFlip) then\n checkScale.x = -checkScale.x\n rotationZ = 180\n end\n for x = 1, check.array.x do\n for y = 1, check.array.y do\n local arrayID = x + (y - 1) * check.array.x\n local func = \"MarumEditableClickCheckbox_\" .. checkID .. \"_\" .. arrayID\n if (check.locked == \"True\" or sheetLocked or locks.checks) then\n func = \"null\"\n end\n local rotationY = 0\n local posMul = 1\n if (flip == \"True\") then\n rotationY = 180\n posMul = -posMul\n end\n local pos = getCheckPosition(checkID, x, y)\n local bindex = buttonIndex\n _G['MarumEditableClickCheckbox_' .. checkID .. '_' .. arrayID] = function(obj, ply, alt)\n local fID = checkID\n local aID = arrayID\n local bi = bindex\n obj.call(\"clickcheck\", { fID, aID, alt, bi, ply })\n end\n\n local tooltip = getCheckTooltip(checkID)\n local label, color, alphaCorrectedColor = getCheckLabelAndColor(checkID, arrayID)\n buttonIndex = buttonIndex + 1\n lookupButtonIndexToInfo[buttonIndex] = { type = \"check\", id = checkID, arrayID = arrayID }\n table.insert(lookupCheckIndices[checkID].buttons, { index = buttonIndex, x = x, y = y, arrayID = arrayID })\n self.createButton({\n label = label,\n tooltip = tooltip,\n click_function = func,\n function_owner = self,\n alignment = 3,\n position = pos,\n width = 250,\n height = 250,\n rotation = { x = 0, y = rotationY, z = rotationZ },\n font_size = check.font,\n scale = { x = checkScale.x, y = checkScale.y, z = checkScale.z },\n font_color = alphaCorrectedColor,\n color = check.checkColor,\n tab = 0\n })\n\n if (buttonIndex % 10 == 0) then coroutine.yield(0) end\n end\n end\n end\n if (not getCommited() and editingSheet) then\n createSelectionButtons()\n end\n creating = false\n return 1\nend\n\nfunction revertField(args)\n local fieldID = args[1]\n local ply = args[2]\n local selected = args[4]\n if (not selected) then\n Wait.frames(\n function()\n for k, v in pairs(lookupFieldIndices[fieldID].inputs) do\n self.editInput({ index = v.index - 1, value = fields[fieldID].value[v.arrayID] })\n end\n end,\n 1\n )\n else\n if (lastFieldLockedMessage ~= fieldID) then\n broadcastToColor(\"This text is locked\", ply, { r = 1, g = 0.5, b = 0 })\n lastFieldLockedMessage = fieldID\n end\n end\nend\n\nfunction recalculateVSums(fieldID)\n local field = fields[fieldID]\n for k, v in pairs(lookupFieldIndices[fieldID].totals) do\n local vsum = 0\n for y = 1, field.array.y do\n local arrayID = v.x + (y - 1) * field.array.x\n if (tonumber(field.value[arrayID])) then\n vsum = vsum + tonumber(field.value[arrayID])\n end\n end\n self.editInput({ index = v.index - 1, value = \"[u]\" .. vsum .. \"[/u]\" })\n end\nend\n\nfunction revertFieldSum(args)\n local fieldID = args[1]\n local ply = args[2]\n local selected = args[4]\n if (not selected) then\n Wait.frames(\n function()\n recalculateVSums(fieldID)\n end,\n 1\n )\n else\n if (lastFieldLockedMessage ~= fieldID) then\n broadcastToColor(\"This text is reserved for the total sum\", ply, { r = 1, g = 1, b = 0 })\n lastFieldLockedMessage = fieldID\n end\n end\nend\n\nfunction createDecals()\n local decalParameters = {}\n for decalID, decal in pairs(decals) do\n local rotationAdd = 180\n if (flip == \"True\") then\n rotationAdd = 0\n end\n local pos = getDecalPosition(decalID)\n table.insert(decalParameters, {\n url = decal.url,\n name = \"Image #\" .. decalID,\n position = { x = -pos.x, y = pos.y, z = pos.z },\n rotation = { x = 90, y = rotationAdd + decal.rotation, z = 0 },\n scale = { x = decal.scale.x * scale.x, y = decal.scale.y * scale.y, z = 1 }\n })\n end\n self.setDecals(decalParameters)\nend\n\nfunction getFieldPosition(fieldID, x, y)\n local field = fields[fieldID]\n local mul = 1\n if (flip == \"True\") then mul = -1 end\n return {\n x = (field.pos.x + (x - 1) * field.distance.x) * scale.x * mul,\n y = height + 0.002,\n z = (field.pos.y + (y - 1) * field.distance.y) * scale.y * mul\n }\nend\n\nfunction getFieldTooltip(fieldID, arrayID)\n local field = fields[fieldID]\n local tooltip = \"\"\n if (not field.locked) then\n if (field.tooltip == nil or field.tooltip:find(\"name\")) then\n tooltip = field.name or \"\"\n elseif (field.tooltip:find(\"content\")) then\n tooltip = field.value[arrayID]\n end\n end\n return tooltip\nend\n\nfunction getCheckPosition(checkID, x, y)\n local check = checks[checkID]\n local mul = 1\n if (flip == \"True\") then mul = -1 end\n return {\n x = (check.pos.x + (x - 1) * check.distance.x) * mul * scale.x,\n y = height + 0.002,\n z = (check.pos.y + (y - 1) * check.distance.y) *\n mul * scale.y\n }\nend\n\nfunction getDecalPosition(decalID)\n local decal = decals[decalID]\n local mul = 1\n if (flip == \"True\") then mul = -1 end\n return { x = decal.pos.x * mul * scale.x, y = height + 0.005, z = decal.pos.y * mul * scale.y }\nend\n\nfunction getDecalTooltip(decalID)\n local decal = decals[decalID]\n local tooltip = \"\"\n if (decal.tooltip == nil) then\n tooltip = decal.name or \"\"\n elseif (decal.tooltip:find(\"name\")) then\n tooltip = decal.name or \"\"\n elseif (decal.tooltip:find(\"hint\")) then\n tooltip = \"Click to change image\"\n end\n return tooltip\nend\n\nfunction increaseCounter(args)\n local fieldID = tonumber(args[1])\n local arrayID = tonumber(args[2])\n local ply = args[3]\n local field = fields[fieldID]\n if (field.value[arrayID] == nil or field.value[arrayID] == \"\") then\n field.value[arrayID] = 0\n end\n if (tonumber(field.value[arrayID])) then\n field.value[arrayID] = tonumber(field.value[arrayID]) + 1\n updateFieldNameContentAndTooltip(fieldID)\n if (field.vsum == \"True\") then\n recalculateVSums(fieldID)\n end\n updateSave()\n else\n broadcastToColor(\"Field does not contain a valid number\", ply)\n end\nend\n\nfunction decreaseCounter(args)\n local fieldID = tonumber(args[1])\n local arrayID = tonumber(args[2])\n local ply = args[3]\n local field = fields[fieldID]\n if (field.value[arrayID] == nil or field.value[arrayID] == \"\") then\n field.value[arrayID] = 0\n end\n if (tonumber(field.value[arrayID])) then\n field.value[arrayID] = tonumber(field.value[arrayID]) - 1\n updateFieldNameContentAndTooltip(fieldID)\n if (field.vsum == \"True\") then\n recalculateVSums(fieldID)\n end\n updateSave()\n else\n broadcastToColor(\"Field does not contain a valid number\", ply)\n end\nend\n\nfunction edit(obj, ply, value, selected)\n for k, v in pairs(obj.getInputs()) do\n if (lookupInputIndexToInfo[k] ~= nil) then\n if (lookupInputIndexToInfo[k].type == \"field\") then\n local field = fields[lookupInputIndexToInfo[k].id]\n if (v.value ~= field.value[lookupInputIndexToInfo[k].arrayID]) then\n field.value[lookupInputIndexToInfo[k].arrayID] = v.value\n if (field.tooltip ~= nil) then\n if (field.tooltip:find(\"content\")) then\n self.editInput({ index = k - 1, tooltip = v.value })\n end\n end\n if (field.role ~= nil) then\n if (field.role:find(\"name\")) then\n self.setName(value)\n elseif (field.role:find(\"description\")) then\n self.setDescription(value)\n end\n end\n updateSave()\n break\n end\n end\n end\n end\nend\n\nfunction editUrl(obj, ply, value, selected)\n for k, v in pairs(obj.getInputs()) do\n if (lookupInputIndexToInfo[k] ~= nil) then\n if (lookupInputIndexToInfo[k].type == \"decal\") then\n if (v.value ~= decals[lookupInputIndexToInfo[k].id].url) then\n decals[lookupInputIndexToInfo[k].id].url = v.value\n updateSave()\n createDecals()\n break\n end\n end\n end\n end\nend\n\nfunction clickcheck(args)\n local checkID = tonumber(args[1])\n local arrayID = tonumber(args[2])\n local alt_click = args[3]\n local buttonIndex = args[4]\n local ply = args[5]\n local check = checks[checkID]\n local value = tonumber(check.value[arrayID])\n if (value == nil) then value = 1 end\n if (alt_click) then\n if (value \u003e 0) then\n check.value[arrayID] = 0\n else\n check.value[arrayID] = 1\n end\n else\n if (value == 1) then\n check.value[arrayID] = 2\n else\n if (value == 2) then\n check.value[arrayID] = 1\n elseif (check.fillFromDisabled == \"True\") then\n check.value[arrayID] = 2\n else\n broadcastToColor(\"This checkbox is disabled. You can enable/disable checkboxes with Right click.\", ply)\n end\n end\n end\n\n local label, color, alphaCorrectedColor = getCheckLabelAndColor(checkID, arrayID)\n self.editButton({ index = buttonIndex, label = label, font_color = alphaCorrectedColor })\n updateSave()\nend\n\nfunction getFieldTextColor(fieldID)\n local field = fields[fieldID]\n local textAlpha = field.textColor.a\n if (tonumber(field.fieldColor.a) \u003e 0) then\n textAlpha = tonumber(field.textColor.a) / tonumber(field.fieldColor.a)\n else\n textAlpha = tonumber(field.textColor.a) * 100\n end\n return { r = field.textColor.r, g = field.textColor.g, b = field.textColor.b, a = textAlpha }\nend\n\nfunction getCheckTooltip(checkID)\n local check = checks[checkID]\n local tooltip = \"\"\n if (not check.locked) then\n if (check.tooltip == nil or check.tooltip:find(\"name\")) then\n tooltip = check.name\n elseif (check.tooltip:find(\"hint\")) then\n tooltip = \"Left click to toggle, Right click to enable/disable\"\n end\n end\n return tooltip\nend\n\nfunction getCheckLabelAndColor(checkID, arrayID)\n local check = checks[checkID]\n local label = check.characters.empty\n if (check.value[arrayID] == 0) then\n label = check.characters.disabled\n elseif (check.value[arrayID] == 2) then\n label = check.characters.filled\n end\n\n local color = nil\n if (check.separateColors == \"True\") then\n if (check.value[arrayID] == 0) then\n color = check.textColorDisabled or check.textColor\n elseif (check.value[arrayID] == 1) then\n color = check.textColorOff or check.textColor\n elseif (check.value[arrayID] == 2) then\n color = check.textColorOn or check.textColor\n else\n color = check.textColorOff or check.textColor\n end\n else\n color = check.textColorOn or check.textColor\n end\n\n local alpha = 1\n local checkAlpha = math.max(1 / 255, check.checkColor.a)\n if (tonumber(check.checkColor.a) \u003e 0) then\n alpha = tonumber(color.a) / tonumber(checkAlpha)\n else\n alpha = tonumber(color.a) * 100\n end\n return label, color, { r = color.r, g = color.g, b = color.b, a = color.a * alpha }\nend\n\nfunction split(inputstr, sep)\n if sep == nil then\n sep = \"%s\"\n end\n local t = {}\n for str in string.gmatch(inputstr, \"([^\" .. sep .. \"]+)\") do\n table.insert(t, str)\n end\n return t\nend\n\nfunction onRotate(spin, flip, player_color, old_spin, old_flip)\n updateLockedFieldOrientation(flip \u003c 90)\nend\n\nfunction onDrop(ply)\n updateLockedFieldOrientation(self.getTransformUp().y \u003e= 0)\nend\n\nfunction updateLockedFieldOrientation(upright)\n for k, v in pairs(fields) do\n if (v.locked == \"True\" or sheetLocked or locks.fields or v.vsum == \"True\") then\n local field = v\n local fieldScale = { x = scale.x, y = 1, z = scale.y }\n local rotation = { x = 0, y = 0, z = 0 }\n if (flip == \"True\") then\n rotation.y = 180\n end\n if (upright) then\n rotation.z = 180\n fieldScale.x = -scale.x\n end\n if (v.locked == \"True\" or sheetLocked or locks.fields) then\n for arrayID, inp in pairs(lookupFieldIndices[k].inputs) do\n self.editInput({ index = inp.index - 1, rotation = rotation, scale = fieldScale })\n end\n end\n for arrayID, inp in pairs(lookupFieldIndices[k].totals) do\n self.editInput({ index = inp.index - 1, rotation = rotation, scale = fieldScale })\n end\n end\n end\n for k, v in pairs(checks) do\n if (v.locked == \"True\" or sheetLocked or locks.fields) then\n local check = v\n local rotationZ = 0\n local checkScale = { x = scale.x * check.size.x, y = 1, z = scale.y * check.size.y }\n if (upright) then\n checkScale.x = -checkScale.x\n rotationZ = 180\n end\n local rotationY = 0\n if (flip == \"True\") then\n rotationY = 180\n end\n for arrayID, inp in pairs(lookupCheckIndices[k].buttons) do\n self.editButton({\n index = inp.index - 1,\n rotation = { x = 0, y = rotationY, z = rotationZ },\n scale = checkScale\n })\n end\n end\n end\nend\n\nfunction showImageURLPanel(args)\n local decalID = args[1]\n local ply = args[2]\n Player[ply].showInputDialog(\"Set image URL\",\n function(text, player_color)\n decals[decalID].url = text\n createDecals()\n end\n )\nend\n\nfunction updateFieldNameContentAndTooltip(fieldID)\n local field = fields[fieldID]\n for k, v in pairs(lookupFieldIndices[fieldID].inputs) do\n self.editInput({\n index = v.index - 1,\n value = field.value[v.arrayID],\n tooltip = getFieldTooltip(fieldID,\n v.arrayID)\n })\n end\n local name = \"T\" .. fieldID\n local tooltip = \"Select \" .. (field.name or name)\n for k, v in pairs(lookupFieldIndices[fieldID].selectionButtons) do\n self.editButton({ index = v.index - 1, tooltip = tooltip })\n end\nend\n\nfunction getCommited()\n return true\nend\n\n--$\n\nfunction getCommited()\n return false\nend\n\nfunction get_line_count(str)\n local lines = 1\n for i = 1, #str do\n local c = str:sub(i, i)\n if c == 'n' then lines = lines + 1 end\n end\n return lines\nend\n\nfunction createSelectionButtons()\n coroutine.yield(0)\n local rotation = { x = 0, y = 0, z = 0 }\n if (flip == \"True\") then\n rotation.y = 180\n end\n\n for fieldID, field in pairs(fields) do\n local posx = field.pos.x\n local posy = field.pos.y\n local fieldScale = { x = scale.x, y = 1, z = scale.y }\n local posMul = 1\n if (flip == \"True\") then\n posMul = -1\n end\n\n local fontSize = field.font\n fontSize = math.min(field.size.y - 23, fontSize)\n for x = 1, field.array.x do\n for y = 1, field.array.y do\n local name = \"T\" .. fieldID\n local arrayID = x + (y - 1) * field.array.x\n local tooltip = \"Select \" .. (field.name or name)\n local pos = {\n x = (posx + (x - 1) * field.distance.x) * fieldScale.x * posMul,\n y = height + 0.005,\n z = (posy + (y - 1) * field.distance.y) * fieldScale.z * posMul\n }\n buttonIndex = buttonIndex + 1\n table.insert(lookupFieldIndices[fieldID].selectionButtons, { index = buttonIndex, x = x, y = y })\n self.createButton({\n click_function = 'MarumEditableSheet_SelectField/' .. fieldID .. '_' .. arrayID,\n tooltip = tooltip,\n function_owner = self,\n label = \"\",\n position = pos,\n rotation = rotation,\n scale = { x = fieldScale.x * 0.25, y = 1, z = fieldScale.z * 0.25 },\n width = field.size.x * 4 + 200,\n height = field.size.y * 4 + 200,\n font_size = field.font,\n font_color = { r = 1, g = 1, b = 1, a = 1 },\n color = { r = 0, g = 0.5, b = 1, a = 0.5 },\n hover_color = { r = 0, g = 0.5, b = 1, a = 0.8 }\n })\n _G['MarumEditableSheet_SelectField/' .. fieldID .. '_' .. arrayID] = function(obj, ply, alt)\n obj.call(\"selectField\", { id = fieldID, arrayId = arrayID })\n end\n end\n end\n if (buttonIndex % 10 == 0) then coroutine.yield(0) end\n end\n\n for decalID, decal in pairs(decals) do\n local name = \"D\" .. decalID\n local tooltip = \"Select \" .. (decal.name or name)\n local pos = getDecalPosition(decalID)\n buttonIndex = buttonIndex + 1\n table.insert(lookupDecalIndices[decalID].selectionButtons, { index = buttonIndex, x = x, y = y })\n self.createButton({\n click_function = 'MarumEditableSheet_SelectDecal_' .. decalID,\n tooltip = tooltip,\n function_owner = self,\n label = \"\",\n position = pos,\n rotation = rotation,\n scale = { x = decal.scale.x * scale.x * 0.25, y = decal.scale.y * scale.y * 0.25, z = decal.scale.y * scale.y * 0.25 },\n width = 2000,\n height = 2000,\n font_size = 100,\n font_color = { r = 1, g = 1, b = 1, a = 1 },\n color = { r = 0, g = 0.5, b = 1, a = 0.5 },\n hover_color = { r = 0, g = 0.5, b = 1, a = 0.8 }\n })\n _G['MarumEditableSheet_SelectDecal_' .. decalID] = function(obj, ply, alt)\n obj.call(\"selectDecal\", decalID)\n end\n if (buttonIndex % 10 == 0) then coroutine.yield(0) end\n end\n\n for checkID, check in pairs(checks) do\n local posx = check.pos.x\n local posy = check.pos.y\n local checkScale = { x = scale.x * check.size.x, y = 1, z = scale.y * check.size.y }\n local name = \"C\" .. checkID\n local tooltip = \"Select \" .. (check.name or name)\n for x = 1, check.array.x do\n for y = 1, check.array.y do\n local arrayID = x + (y - 1) * check.array.x\n local func = \"MarumEditableClickCheckbox_\" .. checkID .. '_' .. arrayID\n local rotationAdd = 0\n local posMul = 1\n if (flip == \"True\") then\n rotationAdd = 180\n posMul = -1\n end\n local pos = {\n x = (posx + (x - 1) * check.distance.x) * posMul * scale.x,\n y = height + 0.002,\n z = (posy + (y - 1) * check.distance.y) * posMul * scale.y\n }\n buttonIndex = buttonIndex + 1\n table.insert(lookupCheckIndices[checkID].selectionButtons, {\n index = buttonIndex,\n x = x,\n y = y,\n arrayID = arrayID\n })\n self.createButton({\n label = \"\",\n tooltip = tooltip,\n click_function = func,\n function_owner = self,\n alignment = 3,\n position = pos,\n width = 1000,\n height = 1000,\n rotation = { x = 0, y = rotationAdd, z = 0 },\n font_size = check.font / 3,\n scale = { x = checkScale.x * 0.25, y = checkScale.y, z = checkScale.z * 0.25 },\n font_color = { r = 1, g = 1, b = 1, a = 1 },\n color = { r = 0, g = 0.5, b = 1, a = 0.5 },\n hover_color = { r = 0, g = 0.5, b = 1, a = 0.8 },\n tab = 0\n })\n _G['MarumEditableClickCheckbox_' .. checkID .. '_' .. arrayID] = function(obj, ply, alt)\n obj.call(\"selectCheck\", { id = checkID, arrayId = arrayID })\n end\n end\n end\n if (buttonIndex % 10 == 0) then coroutine.yield(0) end\n end\n\n UI.setAttribute(getPanelId(\"Loading\"), \"active\", \"False\")\nend\n\nfunction createSelectionHighlight(edit)\n local topLeft = { x = 0, y = 0, z = 0 }\n local topRight = { x = 0, y = 0, z = 0 }\n local bottomLeft = { x = 0, y = 0, z = 0 }\n local bottomRight = { x = 0, y = 0, z = 0 }\n local scale = { x = scale.x, y = 1, z = scale.y }\n local rotation = { x = 0, y = 0, z = 0 }\n if (flip == \"True\") then\n rotation.y = 180\n end\n\n if (selectedType == \"field\") then\n local field = fields[selectedId]\n local posx = field.pos.x\n local posy = field.pos.y\n local posMul = 1\n if (flip == \"True\") then\n posMul = -1\n posy = posy - 0.1\n end\n local x = (selectedArrayId - 1) % field.array.x\n local y = math.floor((selectedArrayId - 1) / field.array.x)\n local gridpos = { x = x * field.distance.x, y = y * field.distance.y }\n bottomLeft = {\n x = (posx - field.size.x / 1000 - 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy + field.size.y / 1000 + 0.1 + gridpos.y) * scale.z * posMul\n }\n bottomRight = {\n x = (posx + field.size.x / 1000 + 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy + field.size.y / 1000 + 0.1 + gridpos.y) * scale.z * posMul\n }\n topLeft = {\n x = (posx - field.size.x / 1000 - 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy - field.size.y / 1000 + gridpos.y) * scale.z * posMul\n }\n topRight = {\n x = (posx + field.size.x / 1000 + 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy - field.size.y / 1000 + gridpos.y) * scale.z * posMul\n }\n elseif (selectedType == \"decal\") then\n local decal = decals[selectedId]\n local posx = decal.pos.x\n local posy = decal.pos.y\n local posMul = 1\n if (flip == \"True\") then\n posMul = -1\n posy = posy - 0.1\n end\n bottomLeft = {\n x = (posx - decal.scale.x / 2 - 0.1) * scale.x * posMul,\n y = height + 0.005,\n z = (posy + decal.scale.y / 2 + 0.1) * scale.z * posMul\n }\n bottomRight = {\n x = (posx + decal.scale.x / 2 + 0.1) * scale.x * posMul,\n y = height + 0.005,\n z = (posy + decal.scale.y / 2 + 0.1) * scale.z * posMul\n }\n topLeft = {\n x = (posx - decal.scale.x / 2 - 0.1) * scale.x * posMul,\n y = height + 0.005,\n z = (posy - decal.scale.y / 2) * scale.z * posMul\n }\n topRight = {\n x = (posx + decal.scale.x / 2 + 0.1) * scale.x * posMul,\n y = height + 0.005,\n z = (posy - decal.scale.y / 2) * scale.z * posMul\n }\n elseif (selectedType == \"check\") then\n local check = checks[selectedId]\n local posx = check.pos.x\n local posy = check.pos.y\n local posMul = 1\n if (flip == \"True\") then\n posMul = -1\n posy = posy - 0.1\n end\n local x = (selectedArrayId - 1) % check.array.x\n local y = math.floor((selectedArrayId - 1) / check.array.x)\n local gridpos = { x = x * check.distance.x, y = y * check.distance.y }\n bottomLeft = {\n x = (posx - check.size.x * 0.25 - 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy + check.size.y * 0.25 + 0.1 + gridpos.y) * scale.z * posMul\n }\n bottomRight = {\n x = (posx + check.size.x * 0.25 + 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy + check.size.y * 0.25 + 0.1 + gridpos.y) * scale.z * posMul\n }\n topLeft = {\n x = (posx - check.size.x * 0.25 - 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy - check.size.y * 0.25 + gridpos.y) * scale.z * posMul\n }\n topRight = {\n x = (posx + check.size.x * 0.25 + 0.1 + gridpos.x) * scale.x * posMul,\n y = height + 0.005,\n z = (posy - check.size.y * 0.25 + gridpos.y) * scale.z * posMul\n }\n end\n if (edit) then\n self.editButton({ index = 0, scale = scale, position = bottomLeft })\n self.editButton({ index = 1, scale = scale, position = topLeft })\n self.editButton({ index = 2, scale = scale, position = bottomRight })\n self.editButton({ index = 3, scale = scale, position = topRight })\n else\n local label = \"┗\"\n if (flip == \"True\") then label = \"┓\" end\n self.createButton({\n click_function = 'null',\n function_owner = self,\n label = label,\n position = bottomLeft,\n rotation = rotation,\n scale = scale,\n width = 0,\n height = 0,\n font_size = 200,\n font_color = { r = 0, g = 0.5, b = 1, a = 100 },\n color = { r = 0, g = 0, b = 0, a = 0.01 },\n })\n label = \"┏\"\n if (flip == \"True\") then label = \"┛\" end\n self.createButton({\n click_function = 'null',\n function_owner = self,\n label = label,\n position = topLeft,\n rotation = rotation,\n scale = scale,\n width = 0,\n height = 0,\n font_size = 200,\n font_color = { r = 0, g = 0.5, b = 1, a = 100 },\n color = { r = 0, g = 0, b = 0, a = 0.01 },\n })\n label = \"┛\"\n if (flip == \"True\") then label = \"┏\" end\n self.createButton({\n click_function = 'null',\n function_owner = self,\n label = label,\n position = bottomRight,\n rotation = rotation,\n scale = scale,\n width = 0,\n height = 0,\n font_size = 200,\n font_color = { r = 0, g = 0.5, b = 1, a = 100 },\n color = { r = 0, g = 0, b = 0, a = 0.01 },\n })\n label = \"┓\"\n if (flip == \"True\") then label = \"┗\" end\n self.createButton({\n click_function = 'null',\n function_owner = self,\n label = label,\n position = topRight,\n rotation = rotation,\n scale = scale,\n width = 0,\n height = 0,\n font_size = 200,\n font_color = { r = 0, g = 0.5, b = 1, a = 100 },\n color = { r = 0, g = 0, b = 0, a = 0.01 },\n })\n buttonIndex = buttonIndex + 4\n end\nend\n\nfunction selectField(args)\n selectedId = args.id\n selectedArrayId = args.arrayId\n selectedType = \"field\"\n createSelectionHighlight(true)\n UI.hide(attrId(\"EmptyPrompt\"))\n UI.show(attrId(\"SelectionPanel\"))\n refreshEditPanel()\nend\n\nfunction selectDecal(id)\n selectedId = id\n selectedType = \"decal\"\n createSelectionHighlight(true)\n UI.hide(attrId(\"EmptyPrompt\"))\n UI.show(attrId(\"SelectionPanel\"))\n refreshEditPanel()\nend\n\nfunction selectCheck(args)\n selectedId = args.id\n selectedArrayId = args.arrayId\n selectedType = \"check\"\n createSelectionHighlight(true)\n UI.hide(attrId(\"EmptyPrompt\"))\n UI.show(attrId(\"SelectionPanel\"))\n refreshEditPanel()\nend\n\nfunction refreshEditPanel()\n local page = selectedId\n local subpages = 0\n if (selectedType == \"field\") then\n local field = fields[selectedId]\n local name = \"\"\n if (field.name ~= \"\" and field.name ~= nil) then\n name = \" - \" .. field.name\n end\n local value = field.value[selectedArrayId]\n subpages = field.array.x * field.array.y\n local sub = \"\"\n if (subpages \u003e 1) then\n sub = \";\" .. selectedArrayId\n end\n UI.setAttribute(attrId(\"Field/ID\"), \"text\", \"\u003cb\u003eText #\" .. tostring(selectedId) .. sub .. name .. \"\u003c/b\u003e\")\n UI.setAttribute(attrId(\"Field/name\"), \"text\", field.name)\n UI.setAttribute(attrId(\"Field/content\"), \"text\", value or \"\")\n UI.setAttribute(attrId(\"Field/font\"), \"text\", field.font)\n UI.setAttribute(attrId(\"Field/pos/x\"), \"text\", field.pos.x)\n UI.setAttribute(attrId(\"Field/pos/y\"), \"text\", field.pos.y)\n UI.setAttribute(attrId(\"Field/size/x\"), \"text\", field.size.x)\n UI.setAttribute(attrId(\"Field/size/y\"), \"text\", field.size.y)\n UI.setAttribute(attrId(\"Field/array/x\"), \"text\", field.array.x)\n UI.setAttribute(attrId(\"Field/array/y\"), \"text\", field.array.y)\n UI.setAttribute(attrId(\"Field/distance/x\"), \"text\", field.distance.x)\n UI.setAttribute(attrId(\"Field/distance/y\"), \"text\", field.distance.y)\n\n local textColor = \"rgba(\" .. field.textColor.r .. \",\" .. field.textColor.g .. \",\" .. field.textColor.b .. \",1)\"\n local textColor2 = \"rgba(\" ..\n (field.textColor.r * 0.5 + 0.2) .. \",\" .. (field.textColor.g * 0.5 + 0.2) ..\n \",\" .. (field.textColor.b * 0.5 + 0.2) .. \",1)\"\n UI.setAttribute(attrId(\"Field/textColor\"), \"colors\", textColor ..\n \"|\" .. textColor2 .. \"|\" .. textColor2 .. \"|\" .. textColor)\n UI.setAttribute(attrId(\"Field/textColor/a\"), \"percentage\", field.textColor.a * 100)\n\n local fieldColor = \"rgba(\" .. field.fieldColor.r .. \",\" .. field.fieldColor.g .. \",\" .. field.fieldColor.b ..\n \",1)\"\n local fieldColor2 = \"rgba(\" ..\n (field.fieldColor.r * 0.5 + 0.2) .. \",\" .. (field.fieldColor.g * 0.5 + 0.2) ..\n \",\" .. (field.fieldColor.b * 0.5 + 0.2) .. \",1)\"\n UI.setAttribute(attrId(\"Field/fieldColor\"), \"colors\",\n fieldColor .. \"|\" .. fieldColor2 .. \"|\" .. fieldColor2 .. \"|\" .. fieldColor)\n UI.setAttribute(attrId(\"Field/fieldColor/a\"), \"percentage\", field.fieldColor.a * 100)\n\n UI.setAttribute(attrId(\"Field/counter\"), \"isOn\", field.counter == \"True\")\n UI.setAttribute(attrId(\"Field/vsum\"), \"isOn\", field.vsum == \"True\")\n UI.setAttribute(attrId(\"Field/locked\"), \"isOn\", field.locked == \"True\")\n\n local role = 0\n if (field.role ~= nil) then\n if (field.role:find(\"name\")) then\n role = 1\n elseif (field.role:find(\"desc\")) then\n role = 2\n end\n end\n UI.setAttribute(attrId(\"Field/role\"), \"value\", role)\n\n local tooltip = 0\n if (field.tooltip ~= nil) then\n if (field.tooltip:find(\"name\")) then\n tooltip = 1\n elseif (field.tooltip:find(\"content\")) then\n tooltip = 2\n end\n end\n UI.setAttribute(attrId(\"Field/tooltip\"), \"value\", tooltip)\n\n local align = 0\n if (field.align == 3) then\n align = 1\n elseif (field.align == 4) then\n align = 2\n end\n UI.setAttribute(attrId(\"Field/align\"), \"value\", align)\n\n UI.hide(attrId(\"CheckPanel\"))\n UI.hide(attrId(\"DecalPanel\"))\n UI.show(attrId(\"FieldPanel\"))\n elseif (selectedType == \"check\") then\n local check = checks[selectedId]\n local name = \"\"\n if (check.name ~= \"\" and check.name ~= \"\") then\n name = \" - \" .. check.name\n end\n subpages = check.array.x * check.array.y\n local sub = \"\"\n if (subpages \u003e 1) then\n sub = \";\" .. selectedArrayId\n end\n UI.setAttribute(attrId(\"Check/ID\"), \"text\", \"\u003cb\u003eCheckbox #\" .. tostring(selectedId) .. sub .. name .. \"\u003c/b\u003e\")\n UI.setAttribute(attrId(\"Check/name\"), \"text\", check.name)\n UI.setAttribute(attrId(\"Check/font\"), \"text\", check.font)\n UI.setAttribute(attrId(\"Check/pos/x\"), \"text\", check.pos.x)\n UI.setAttribute(attrId(\"Check/pos/y\"), \"text\", check.pos.y)\n UI.setAttribute(attrId(\"Check/size/x\"), \"text\", check.size.x)\n UI.setAttribute(attrId(\"Check/size/y\"), \"text\", check.size.y)\n UI.setAttribute(attrId(\"Check/array/x\"), \"text\", check.array.x)\n UI.setAttribute(attrId(\"Check/array/y\"), \"text\", check.array.y)\n UI.setAttribute(attrId(\"Check/distance/x\"), \"text\", check.distance.x)\n UI.setAttribute(attrId(\"Check/distance/y\"), \"text\", check.distance.y)\n\n local tc = check.textColorOn or check.textColor\n local textColor = \"rgba(\" .. tc.r .. \",\" .. tc.g .. \",\" .. tc.b .. \",1)\"\n local textColor2 = \"rgba(\" .. (tc.r * 0.5 + 0.2) .. \",\" .. (tc.g * 0.5 + 0.2) .. \",\" .. (tc.b * 0.5 + 0.2) ..\n \",1)\"\n UI.setAttribute(attrId(\"Check/textColorOn\"), \"colors\",\n textColor .. \"|\" .. textColor2 .. \"|\" .. textColor2 .. \"|\" .. textColor)\n UI.setAttribute(attrId(\"Check/textColorOn/a\"), \"percentage\", tc.a * 100)\n\n local tc = check.textColorOff or check.textColor\n local textColor = \"rgba(\" .. tc.r .. \",\" .. tc.g .. \",\" .. tc.b .. \",1)\"\n local textColor2 = \"rgba(\" .. (tc.r * 0.5 + 0.2) .. \",\" .. (tc.g * 0.5 + 0.2) .. \",\" .. (tc.b * 0.5 + 0.2) ..\n \",1)\"\n UI.setAttribute(attrId(\"Check/textColorOff\"), \"colors\",\n textColor .. \"|\" .. textColor2 .. \"|\" .. textColor2 .. \"|\" .. textColor)\n UI.setAttribute(attrId(\"Check/textColorOff/a\"), \"percentage\", tc.a * 100)\n\n local tc = check.textColorDisabled or check.textColor\n local textColor = \"rgba(\" .. tc.r .. \",\" .. tc.g .. \",\" .. tc.b .. \",1)\"\n local textColor2 = \"rgba(\" .. (tc.r * 0.5 + 0.2) .. \",\" .. (tc.g * 0.5 + 0.2) .. \",\" .. (tc.b * 0.5 + 0.2) ..\n \",1)\"\n UI.setAttribute(attrId(\"Check/textColorDisabled\"), \"colors\",\n textColor .. \"|\" .. textColor2 .. \"|\" .. textColor2 .. \"|\" .. textColor)\n UI.setAttribute(attrId(\"Check/textColorDisabled/a\"), \"percentage\", tc.a * 100)\n\n local checkColor = \"rgba(\" .. check.checkColor.r .. \",\" .. check.checkColor.g .. \",\" .. check.checkColor.b ..\n \",1)\"\n local checkColor2 = \"rgba(\" ..\n (check.checkColor.r * 0.5 + 0.2) .. \",\" .. (check.checkColor.g * 0.5 + 0.2) ..\n \",\" .. (check.checkColor.b * 0.5 + 0.2) .. \",1)\"\n UI.setAttribute(attrId(\"Check/checkColor\"), \"colors\",\n checkColor .. \"|\" .. checkColor2 .. \"|\" .. checkColor2 .. \"|\" .. checkColor)\n UI.setAttribute(attrId(\"Check/checkColor/a\"), \"percentage\", check.checkColor.a * 100)\n\n UI.setAttribute(attrId(\"Check/characters/empty\"), \"text\", check.characters.empty)\n UI.setAttribute(attrId(\"Check/characters/filled\"), \"text\", check.characters.filled)\n UI.setAttribute(attrId(\"Check/characters/disabled\"), \"text\", check.characters.disabled)\n\n if (check.separateColors == \"True\") then\n UI.setAttribute(attrId(\"CheckOnColorLabel\"), \"text\", \"On color\")\n UI.setAttribute(attrId(\"CheckOffColorRow\"), \"active\", \"True\")\n UI.setAttribute(attrId(\"CheckDisabledColorRow\"), \"active\", \"True\")\n UI.setAttribute(attrId(\"CheckSeparateColorsSpacer1\"), \"active\", \"False\")\n UI.setAttribute(attrId(\"CheckSeparateColorsSpacer2\"), \"active\", \"False\")\n else\n UI.setAttribute(attrId(\"CheckOnColorLabel\"), \"text\", \"Color\")\n UI.setAttribute(attrId(\"CheckOffColorRow\"), \"active\", \"False\")\n UI.setAttribute(attrId(\"CheckDisabledColorRow\"), \"active\", \"False\")\n UI.setAttribute(attrId(\"CheckSeparateColorsSpacer1\"), \"active\", \"True\")\n UI.setAttribute(attrId(\"CheckSeparateColorsSpacer2\"), \"active\", \"True\")\n end\n UI.setAttribute(attrId(\"Check/separateColors\"), \"isOn\", check.separateColors == \"True\")\n UI.setAttribute(attrId(\"Check/fillFromDisabled\"), \"isOn\", check.fillFromDisabled == \"True\")\n\n local option = 0\n if (check.value[selectedArrayId] == 0) then option = 2 end\n if (check.value[selectedArrayId] == 1) then option = 0 end\n if (check.value[selectedArrayId] == 2) then option = 1 end\n UI.setAttribute(attrId(\"Check/value\"), \"value\", option)\n\n local tooltip = 0\n if (check.tooltip ~= nil) then\n if (check.tooltip:find(\"name\")) then\n tooltip = 1\n elseif (check.tooltip:find(\"hint\")) then\n tooltip = 2\n end\n end\n UI.setAttribute(attrId(\"Check/tooltip\"), \"value\", tooltip)\n UI.setAttribute(attrId(\"Check/locked\"), \"isOn\", check.locked == \"True\")\n\n UI.hide(attrId(\"DecalPanel\"))\n UI.hide(attrId(\"FieldPanel\"))\n UI.show(attrId(\"CheckPanel\"))\n\n page = selectedId + #fields\n elseif (selectedType == \"decal\") then\n local decal = decals[selectedId]\n local slot = 1\n UI.setAttribute(attrId(\"Decal/ID\"), \"text\", \"\u003cb\u003eDecal #\" .. tostring(selectedId) .. \"\u003c/b\u003e\")\n UI.setAttribute(attrId(\"Decal/name\"), \"text\", decal.name)\n UI.setAttribute(attrId(\"Decal/url\"), \"text\", decal.url)\n UI.setAttribute(attrId(\"Decal/pos/x\"), \"text\", decal.pos.x)\n UI.setAttribute(attrId(\"Decal/pos/y\"), \"text\", decal.pos.y)\n UI.setAttribute(attrId(\"Decal/scale/x\"), \"text\", decal.scale.x)\n UI.setAttribute(attrId(\"Decal/scale/y\"), \"text\", decal.scale.y)\n UI.setAttribute(attrId(\"Decal/rotation\"), \"text\", decal.rotation)\n\n UI.setAttribute(attrId(\"Decal/locked\"), \"isOn\", decal.locked == \"True\")\n\n local tooltip = 0\n if (decal.tooltip ~= nil) then\n if (decal.tooltip:find(\"name\")) then\n tooltip = 1\n elseif (decal.tooltip:find(\"hint\")) then\n tooltip = 2\n end\n end\n UI.setAttribute(attrId(\"Decal/tooltip\"), \"value\", tooltip)\n\n UI.hide(attrId(\"CheckPanel\"))\n UI.hide(attrId(\"FieldPanel\"))\n UI.show(attrId(\"DecalPanel\"))\n page = selectedId + #fields + #checks\n else\n UI.show(attrId(\"EmptyPrompt\"))\n UI.hide(attrId(\"SelectionPanel\"))\n end\n\n -- Pages\n local pageCount = getPageCount()\n UI.setAttribute(attrId(\"LastPage\"), \"interactable\", tostring(page \u003c pageCount))\n UI.setAttribute(attrId(\"NextPage\"), \"interactable\", tostring(page \u003c pageCount))\n UI.setAttribute(attrId(\"PreviousPage\"), \"interactable\", tostring(page \u003e 1))\n UI.setAttribute(attrId(\"FirstPage\"), \"interactable\", tostring(page \u003e 1))\n if (subpages \u003e 1) then\n UI.setAttribute(attrId(\"Pages\"), \"text\", page .. \" (\" .. selectedArrayId .. \"/\" .. subpages ..\n \") / \" .. pageCount)\n else\n UI.setAttribute(attrId(\"Pages\"), \"text\", page .. \" / \" .. pageCount)\n end\nend\n\nfunction commit(ply)\n local codeSplit = split(self.script_code, \"$\")\n local uncutLines = get_line_count(codeSplit[1])\n local cutLines = get_line_count(codeSplit[2] .. codeSplit[3])\n broadcastToColor(\"Layout commited. Code reduced from \" .. (uncutLines + cutLines + 1) .. \" lines to \" .. uncutLines,\n ply)\n if (Global.getVar(\"MarumEditableSheetGUID\", self.guid)) then\n Global.setVar(\"MarumEditableSheetGUID\", nil)\n end\n self.script_code = codeSplit[1]\n self.reload()\nend\n\nfunction onToggleSheetLocked(obj, ply, value, selected)\n sheetLocked = not sheetLocked\n updateSave()\nend\n\nfunction closePanel(ply, value, id)\n if (editingSheet) then\n editingSheet = false\n refresh()\n end\n UI.setAttribute(getPanelId(\"main\"), \"active\", \"False\")\nend\n\nfunction forceClosePanel()\n if (editingSheet) then\n editingSheet = false\n refresh()\n end\nend\n\nfunction getPageCount()\n return #fields + #checks + #decals\nend\n\nfunction getPanelId(panel)\n return \"MarumEditableSheet/\" .. panel\nend\n\nfunction showEditPanel(ply, value, id)\n createPanelIfNecessary(ply)\n waitForUiLoaded(function()\n UI.setAttribute(getPanelId(\"main\"), \"active\", \"True\")\n UI.setAttribute(getPanelId(\"main\"), \"visibility\", ply)\n if (selectedId == 0) then\n UI.show(attrId(\"EmptyPrompt\"))\n UI.hide(attrId(\"SelectionPanel\"))\n else\n UI.hide(attrId(\"EmptyPrompt\"))\n UI.show(attrId(\"SelectionPanel\"))\n end\n editingSheet = true\n local forcedSelectFirst = false\n if (selectedId == 0) then\n if (#fields \u003e 0) then\n selectedId = 1\n selectedArrayId = 1\n selectedType = \"field\"\n forcedSelectFirst = true\n elseif (#checks \u003e 0) then\n selectedId = 1\n selectedArrayId = 1\n selectedType = \"check\"\n forcedSelectFirst = true\n elseif (#decals \u003e 0) then\n selectedId = 1\n selectedType = \"decal\"\n forcedSelectFirst = true\n end\n end\n refresh()\n if (forcedSelectFirst) then\n UI.hide(attrId(\"EmptyPrompt\"))\n UI.show(attrId(\"SelectionPanel\"))\n end\n refreshEditPanel()\n end)\nend\n\nfunction showCommitPanel(ply, value, id)\n local previousGUID = Global.getVar(\"MarumEditableSheetGUID\")\n if (previousGUID) then\n local obj = getObjectFromGUID(previousGUID)\n if (obj ~= nil) then\n getObjectFromGUID(previousGUID).call(\"closePanel\")\n end\n end\n Player[ply].showConfirmDialog(\n \"Do you want to lock the current layout and remove editing related code? You will still be able to change the contents of unlocked fields.\",\n commit)\nend\n\nfunction removeMESPanel()\n local tbl = UI.getXmlTable()\n local removing = false\n local newTable = {}\n for k, v in pairs(tbl) do\n local isTag = false\n if (v.attributes ~= nil) then\n if (v.attributes.id == \"MESStart\") then\n removing = true\n isTag = true\n elseif (v.attributes.id == \"MESEnd\") then\n removing = false\n isTag = true\n end\n end\n if (not isTag and not removing) then\n table.insert(newTable, v)\n end\n end\n UI.setXmlTable({ newTable })\nend\n\nfunction createPanelIfNecessary(color)\n local guid = self.getGUID()\n local previousGUID = Global.getVar(\"MarumEditableSheetGUID\")\n if (guid ~= previousGUID) then\n if (previousGUID ~= nil) then\n local obj = getObjectFromGUID(previousGUID)\n if (obj) then\n getObjectFromGUID(previousGUID).call(\"forceClosePanel\")\n end\n removeMESPanel()\n end\n waitForUiLoaded(createMainPanel)\n end\nend\n\nfunction createMainPanel()\n local guid = self.getGUID()\n Global.setVar(\"MarumEditableSheetGUID\", guid)\n local xml = [[\n \u003cPanel id=\"MESStart\"\u003e\u003c/Panel\u003e\n \u003cPanel\n color=\"#181818\"\n width=\"800\"\n height=\"700\"\n allowDragging=\"true\"\n returnToOriginalPositionWhenReleased=\"false\"\n restrictDraggingToParentBounds=\"false\"\n class=\"MarumEditableSheet\"\n rectAlignment=\"MiddleRight\"\n active=\"false\"\n id=\"]] .. getPanelId(\"main\") .. [[\"\n \u003e\n \u003cText\n rectAlignment=\"UpperLeft\"\n offsetXY=\"-300 330\"\n color=\"#8b8b8b\"\n \u003eEditable Sheet - by Marum\u003c/Text\u003e\n\n \u003cButton\n rectAlignment=\"UpperRight\"\n offsetXY=\"-20 -5\"\n height=\"30\"\n width=\"50\"\n textColor=\"#ffffff\"\n colors=\"#bf1f1f|#2f2f2f|#3f3f3f|#1f1f1f\"\n onClick=\"]] .. guid .. [[/closePanel\"\n \u003eX\u003c/Button\u003e\n\n \u003cPanel padding=\"10 10 40 10\" color=\"#00000000\"\u003e\n \u003cVerticalLayout spacing=\"15\"\u003e\n \u003cTableLayout minHeight=\"30\" spacing=\"3\" cellBackgroundColor=\"#00000000\" \u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"4\"\u003e\u003cText alignment=\"MiddleRight\" color=\"#ffffff\"\u003eDocument settings: \u003c/Text\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cText alignment=\"MiddleRight\" color=\"#ffffff\"\u003eHeight \u003c/Text\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cInputField resizeTextForBestFit=\"True\" onEndEdit=\"]] ..\n guid ..\n [[/onHeightChanged\" tooltipPosition=\"Above\" tooltip=\"How high the fields will appear\" characterValidation=\"Decimal\"\u003e]] ..\n height .. [[\u003c/InputField\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cText alignment=\"MiddleRight\" color=\"#ffffff\"\u003eScale \u003c/Text\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cInputField id=\"]] ..\n attrId(\"ScaleX\") ..\n [[\" resizeTextForBestFit=\"True\" onEndEdit=\"]] ..\n guid ..\n [[/onScaleChangedX\" tooltipPosition=\"Above\" tooltip=\"Horizontal scale\" characterValidation=\"Decimal\"\u003e]] ..\n scale.x .. [[\u003c/InputField\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cInputField id=\"]] ..\n attrId(\"ScaleY\") ..\n [[\" resizeTextForBestFit=\"True\" onEndEdit=\"]] ..\n guid ..\n [[/onScaleChangedY\" tooltipPosition=\"Above\" tooltip=\"Vertical scale\" characterValidation=\"Decimal\"\u003e]] ..\n scale.y .. [[\u003c/InputField\u003e\u003c/Cell\u003e\n \u003cCell\u003e\u003cButton fontSize=\"18\" fontStyle=\"Bold\" onClick=\"]] ..\n guid ..\n [[/onScaleReset\" tooltipBackground=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Reset scale to preserve aspect ratio on stretched objects\"\u003e↺\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"4\"\u003e\u003cToggle textColor=\"#ffffff\" tooltip=\"Prevents users from changing any texts, images or check boxes\" tooltipPosition=\"Above\" isOn=\"]] ..\n tostring(sheetLocked) .. [[\" onValueChanged=\"]] .. guid .. [[/onToggleSheetLocked\"\u003eLock everything\u003c/Toggle\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cToggle textColor=\"#ffffff\" isOn=\"]] ..\n tostring(flip) .. [[\" onValueChanged=\"]] .. guid .. [[/onFlip\"\u003eFlip\u003c/Toggle\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n\n \u003cHorizontalLayout\u003e\n \u003cButton\n textColor=\"#ffffff\"\n colors=\"#1f1f1f|#2f2f2f|#3f3f3f|#1f1f1f\"\n onClick=\"]] .. guid .. [[/addField\"\n preferredHeight=\"45\"\n \u003eAdd new Text\u003c/Button\u003e\n \u003cButton\n textColor=\"#ffffff\"\n colors=\"#1f1f1f|#2f2f2f|#3f3f3f|#1f1f1f\"\n onClick=\"]] .. guid .. [[/addCheck\"\n preferredHeight=\"45\"\n \u003eAdd new Checkbox\u003c/Button\u003e\n \u003cButton\n textColor=\"#ffffff\"\n colors=\"#1f1f1f|#2f2f2f|#3f3f3f|#1f1f1f\"\n onClick=\"]] .. guid .. [[/addDecal\"\n preferredHeight=\"45\"\n \u003eAdd new Image\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n\n \u003cHorizontalLayout preferredHeight=\"30\"\u003e\n \u003cButton\n id=\"]] .. attrId(\"FirstPage\") .. [[\"\n colors=\"#ffffff|#bbbbbb|#666666|#888888\"\n onClick=\"]] .. guid .. [[/firstPage\"\n preferredHeight=\"30\"\n preferredWidth=\"50\"\n interactable=\"False\"\n \u003e↞ First\u003c/Button\u003e\n \u003cButton\n id=\"]] .. attrId(\"PreviousPage\") .. [[\"\n colors=\"#ffffff|#bbbbbb|#666666|#888888\"\n onClick=\"]] .. guid .. [[/previousPage\"\n preferredHeight=\"30\"\n preferredWidth=\"50\"\n interactable=\"False\"\n \u003e← Previous\u003c/Button\u003e\n \u003cText id=\"]] ..\n attrId(\"Pages\") ..\n [[\" alignment=\"MiddleCenter\" color=\"#ffffff\" preferredWidth=\"100\"\u003e]] .. selectedId ..\n \" / \" .. getPageCount() .. [[\u003c/Text\u003e\n \u003cButton\n id=\"]] .. attrId(\"NextPage\") .. [[\"\n colors=\"#ffffff|#bbbbbb|#666666|#888888\"\n onClick=\"]] .. guid .. [[/nextPage\"\n preferredHeight=\"30\"\n preferredWidth=\"50\"\n interactable=\"False\"\n \u003eNext →\u003c/Button\u003e\n \u003cButton\n id=\"]] .. attrId(\"LastPage\") .. [[\"\n colors=\"#ffffff|#bbbbbb|#666666|#888888\"\n onClick=\"]] .. guid .. [[/lastPage\"\n preferredHeight=\"30\"\n preferredWidth=\"50\"\n interactable=\"False\"\n \u003eLast ↠\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n\n \u003cText color=\"#ffffff\" id=\"]] ..\n attrId(\"EmptyPrompt\") ..\n [[\" preferredHeight=\"600\"\u003eAdd a new element using the buttons above. You can then select elements by clicking on them on the sheet, or with the navigation buttons above.\u003c/Text\u003e\n\n \u003cPanel id=\"]] .. attrId(\"SelectionPanel\") .. [[\" color=\"#00000000\" preferredHeight=\"600\"\u003e\n ]] .. getFieldPanel() .. [[\n ]] .. getCheckPanel() .. [[\n ]] .. getDecalPanel() .. [[\n \u003c/Panel\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Panel\u003e\n \u003cPanel\n id=\"]] .. getPanelId(\"loading\") .. [[\"\n color=\"#181818a0\"\n width=\"800\"\n height=\"700\"\n raycastTarget=\"True\"\n active=\"False\"\n \u003e\n \u003cText color=\"#ffffff\" fontSize=\"30\"\u003eLoading...\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Panel\u003e\n \u003cPanel id=\"MESEnd\"\u003e\u003c/Panel\u003e\n ]]\n UI.setXml(UI.getXml() .. xml)\nend\n\nfunction onScaleReset()\n scale = { x = math.floor(100 / self.getScale().x) / 100, y = math.floor(100 / self.getScale().z) / 100 }\n UI.setAttribute(attrId(\"ScaleX\"), \"text\", scale.x)\n UI.setAttribute(attrId(\"ScaleY\"), \"text\", scale.y)\n refreshAllPositionsAndSize()\n refreshEditPanel()\nend\n\nfunction firstPage()\n if (#fields \u003e 0) then\n selectField({ id = 1, arrayId = 1 })\n elseif (#checks \u003e 0) then\n selectCheck({ id = 1, arrayId = 1 })\n elseif (#decals \u003e 0) then\n selectDecal(1)\n end\nend\n\nfunction previousPage()\n if (selectedType == \"decal\") then\n if (selectedId \u003e 1) then\n selectedId = selectedId - 1\n selectDecal(selectedId)\n else\n if (#checks \u003e 0) then\n selectedType = \"check\"\n selectedId = #checks\n selectCheck({ id = selectedId, arrayId = checks[selectedId].array.x * checks[selectedId].array.y })\n elseif (#fields \u003e 0) then\n selectedType = \"field\"\n selectedId = #fields\n selectField({ id = selectedId, arrayId = fields[selectedId].array.x * fields[selectedId].array.y })\n end\n end\n elseif (selectedType == \"check\") then\n if (selectedArrayId \u003e 1) then\n selectedArrayId = selectedArrayId - 1\n selectCheck({ id = selectedId, arrayId = selectedArrayId })\n elseif (selectedId \u003e 1) then\n selectedId = selectedId - 1\n selectCheck({ id = selectedId, arrayId = checks[selectedId].array.x * checks[selectedId].array.y })\n else\n if (#fields \u003e 0) then\n selectedType = \"field\"\n selectedId = #fields\n selectField({ id = selectedId, arrayId = fields[selectedId].array.x * fields[selectedId].array.y })\n end\n end\n elseif (selectedType == \"field\") then\n if (selectedArrayId \u003e 1) then\n selectedArrayId = selectedArrayId - 1\n selectField({ id = selectedId, arrayId = selectedArrayId })\n elseif (selectedId \u003e 1) then\n selectedId = selectedId - 1\n selectField({ id = selectedId, arrayId = fields[selectedId].array.x * fields[selectedId].array.y })\n end\n end\nend\n\nfunction nextPage()\n if (selectedType == \"field\") then\n if (fields[selectedId].array.x * fields[selectedId].array.y \u003e selectedArrayId) then\n selectedArrayId = selectedArrayId + 1\n selectField({ id = selectedId, arrayId = selectedArrayId })\n elseif (#fields \u003e selectedId) then\n selectedId = selectedId + 1\n selectField({ id = selectedId, arrayId = 1 })\n else\n if (#checks \u003e 0) then\n selectedType = \"check\"\n selectedId = 1\n selectCheck({ id = selectedId, arrayId = 1 })\n elseif (#decals \u003e 0) then\n selectedType = \"decal\"\n selectedId = 1\n selectDecal(1)\n end\n end\n elseif (selectedType == \"check\") then\n if (checks[selectedId].array.x * checks[selectedId].array.y \u003e selectedArrayId) then\n selectedArrayId = selectedArrayId + 1\n selectCheck({ id = selectedId, arrayId = selectedArrayId })\n elseif (#checks \u003e selectedId) then\n selectedId = selectedId + 1\n selectCheck({ id = selectedId, arrayId = 1 })\n else\n if (#decals \u003e 0) then\n selectedType = \"decal\"\n selectedId = 1\n selectDecal(1)\n end\n end\n elseif (selectedType == \"decal\") then\n if (#decals \u003e selectedId) then\n selectedId = selectedId + 1\n selectDecal(selectedId)\n end\n end\nend\n\nfunction lastPage()\n if (#decals \u003e 0) then\n selectDecal(#decals)\n elseif (#checks \u003e 0) then\n local last = checks[#checks]\n selectCheck({ id = #checks, arrayId = last.array.x * last.array.y })\n elseif (#fields \u003e 0) then\n local last = fields[#fields]\n selectField({ id = #fields, arrayId = last.array.x * last.array.y })\n end\nend\n\nfunction onNudgeChanged(ply, value, id)\n nudgeDistance = value\n UI.setAttribute(attrId(\"Field_nudge\"), \"text\", nudgeDistance)\n UI.setAttribute(attrId(\"Check_nudge\"), \"text\", nudgeDistance)\n UI.setAttribute(attrId(\"Decal_nudge\"), \"text\", nudgeDistance)\n updateSave()\nend\n\nfunction compareString(str, list)\n for k, v in pairs(list) do\n if (str == v) then return true end\n end\nend\n\nfunction onValueEdited(ply, value, id)\n local spl = split(id, \"/\")\n local parameter = spl[2]\n local tbl = nil\n if (selectedType == \"field\") then tbl = fields end\n if (selectedType == \"check\") then tbl = checks end\n if (selectedType == \"decal\") then tbl = decals end\n if (parameter == \"content\") then\n tbl[selectedId].value[selectedArrayId] = value\n else\n if (#spl \u003e 2) then\n tbl[selectedId][parameter][spl[3]] = value\n else\n tbl[selectedId][parameter] = value\n end\n end\n updateSave()\n\n if (selectedType == \"field\") then\n if (compareString(parameter, { \"pos\", \"distance\", \"size\" })) then\n updateFieldPositionAndSize(selectedId)\n elseif (compareString(parameter, { \"name\", \"content\", \"tooltip\" })) then\n updateFieldNameContentAndTooltip(selectedId)\n elseif (parameter == \"font\") then\n updateFieldFontAndColor(selectedId)\n else\n refresh()\n end\n elseif (selectedType == \"check\") then\n if (compareString(parameter, { \"pos\", \"distance\", \"size\" })) then\n updateCheckPositionAndSize(selectedId)\n elseif (parameter == \"name\") then\n updateCheckNameContentAndTooltip(selectedId)\n elseif (parameter == \"font\") then\n updateCheckFontAndColor(selectedId)\n else\n refresh()\n end\n elseif (selectedType == \"decal\") then\n if (compareString(parameter, { \"pos\", \"scale\", \"rotation\" })) then\n updateDecalPositionAndSize(selectedId)\n elseif (parameter == \"url\") then\n createDecals()\n else\n refresh()\n end\n end\nend\n\nfunction updateFieldPositionAndSize(fieldID)\n local field = fields[fieldID]\n local lookup = lookupFieldIndices[fieldID]\n local fieldScale = { x = -scale.x, y = 1, z = scale.y }\n local flipped = 1\n local rotation = { x = 0, y = 0, z = 180 }\n if (flip == \"True\") then\n rotation.y = 180\n flipped = -1\n end\n local fontSize = math.min(field.size.y - 24, field.font)\n for k, v in pairs(lookup.inputs) do\n local pos = getFieldPosition(fieldID, v.x, v.y)\n self.editInput({\n index = v.index - 1,\n position = pos,\n scale = fieldScale,\n width = field.size.x,\n height = field.size.y,\n font_size = fontSize,\n rotation = rotation\n })\n end\n for k, v in pairs(lookup.totals) do\n local pos = getFieldPosition(fieldID, v.x, field.array.y + 1)\n self.editInput({\n index = v.index - 1,\n position = pos,\n scale = fieldScale,\n width = field.size.x,\n height = field.size.y,\n rotation = rotation,\n font_size = fontSize\n })\n end\n for k, v in pairs(lookup.counterButtons) do\n local pos = getFieldPosition(fieldID, v.x, v.y)\n local offset = (field.size.x + fontSize * 0.75) / 1000 * scale.x\n self.editButton({\n index = v.index - 1,\n position = { x = pos.x + offset * v.side * flipped, y = pos.y, z = pos.z },\n scale = fieldScale,\n rotation = rotation\n })\n end\n for k, v in pairs(lookup.selectionButtons) do\n local pos = getFieldPosition(fieldID, v.x, v.y)\n self.editButton({\n index = v.index - 1,\n position = pos,\n scale = { x = scale.x * 0.25, y = 1, z = scale.y * 0.25 },\n width = field.size.x * 4 + 200,\n height = field.size.y * 4 + 200\n })\n end\n createSelectionHighlight(true)\nend\n\nfunction updateFieldFontAndColor(fieldID)\n local field = fields[fieldID]\n local fontColor = getFieldTextColor(fieldID)\n local fontSize = math.min(field.size.y - 24, field.font)\n local lookup = lookupFieldIndices[fieldID]\n for k, v in pairs(lookup.inputs) do\n -- Do not question the ways of the tabletop, as if it requests for font_color to be assigned twice, then it shall be so\n self.editInput({\n index = v.index - 1,\n font_color = fontColor\n })\n self.editInput({\n index = v.index - 1,\n font_size = fontSize,\n color = field.fieldColor,\n font_color = fontColor\n })\n self.editInput({\n index = v.index - 1\n })\n end\n for k, v in pairs(lookup.counterButtons) do\n self.editButton({\n index = v.index - 1,\n color = field.fieldColor,\n font_color = fontColor,\n font_size = fontSize / 2\n })\n end\n for k, v in pairs(lookup.totals) do\n self.editInput({\n index = v.index - 1,\n font_size = fontSize,\n color = field.fieldColor,\n font_color = fontColor\n })\n end\nend\n\nfunction updateCheckNameContentAndTooltip(checkID)\n local check = checks[checkID]\n for k, v in pairs(lookupCheckIndices[checkID].buttons) do\n local label = getCheckLabelAndColor(checkID, v.arrayID)\n self.editButton({ index = v.index - 1, label = label, tooltip = getCheckTooltip(checkID) })\n end\n local name = \"C\" .. checkID\n local tooltip = \"Select \" .. (check.name or name)\n for k, v in pairs(lookupCheckIndices[checkID].selectionButtons) do\n self.editButton({ index = v.index - 1, tooltip = tooltip })\n end\nend\n\nfunction updateCheckPositionAndSize(checkID)\n local check = checks[checkID]\n local lookup = lookupCheckIndices[checkID]\n local rotation = { x = 0, y = 0, z = 0 }\n if (flip == \"True\") then\n rotation.y = 180\n end\n for k, v in pairs(lookup.buttons) do\n local pos = getCheckPosition(checkID, v.x, v.y)\n local checkScale = { x = scale.x * check.size.x, y = 1, z = scale.y * check.size.y }\n self.editButton({ index = v.index - 1, position = pos, scale = checkScale, rotation = rotation })\n end\n for k, v in pairs(lookup.selectionButtons) do\n local pos = getCheckPosition(checkID, v.x, v.y)\n local checkScale = { x = scale.x * check.size.x * 0.25, y = 1, z = scale.y * check.size.y * 0.25 }\n self.editButton({ index = v.index - 1, position = pos, scale = checkScale })\n end\n createSelectionHighlight(true)\nend\n\nfunction updateCheckFontAndColor(checkID)\n local check = checks[checkID]\n for k, v in pairs(lookupCheckIndices[checkID].buttons) do\n local label, color, alphaCorrectedColor = getCheckLabelAndColor(checkID, v.arrayID)\n -- Do not question the ways of the tabletop, as if it requests for font_color to be assigned twice, then it shall be so\n self.editButton({\n index = v.index - 1,\n font_size = check.font,\n color = check.checkColor,\n font_color = alphaCorrectedColor\n })\n end\nend\n\nfunction updateDecalPositionAndSize(decalID)\n local decal = decals[decalID]\n local lookup = lookupDecalIndices[decalID]\n local decalScale = {\n x = decal.scale.x * scale.x * 0.25,\n y = decal.scale.y * scale.y * 0.25,\n z = decal.scale.y *\n scale.y * 0.25\n }\n for k, v in pairs(lookup.inputs) do\n local pos = getDecalPosition(decalID, v.x, v.y)\n self.editInput({ index = v.index - 1, position = pos, scale = decalScale, rotation = decal.rotation })\n end\n for k, v in pairs(lookup.selectionButtons) do\n local pos = getDecalPosition(decalID, v.x, v.y)\n self.editButton({ index = v.index - 1, position = pos, scale = decalScale })\n end\n createSelectionHighlight(true)\n createDecals()\nend\n\nfunction onCheckValueEdited(ply, value, id)\n local spl = split(id, \"/\")\n local checkParameter = spl[2]\n if (#spl \u003e 2) then\n checks[selectedId][checkParameter][spl[3]] = value\n else\n checks[selectedId][checkParameter] = value\n end\n\n if (compareString(checkParameter, { \"pos\", \"size\" })) then\n updateCheckPositionAndSize(selectedId)\n elseif (checkParameter == \"characters\") then\n updateCheckNameContentAndTooltip(selectedId)\n else\n refresh()\n end\n\n updateSave()\nend\n\nfunction onDecalValueEdited(ply, value, id)\n local spl = split(id, \"/\")\n local decalID = tonumber(spl[1])\n local decalParameter = spl[2]\n if (#spl \u003e 2) then\n decals[decalID][decalParameter][spl[3]] = value\n else\n decals[decalID][decalParameter] = value\n end\n\n updateSave()\n refresh()\nend\n\nfunction onToggleChanged(ply, value, id)\n local spl = split(id, \"/\")\n local tbl = nil\n if (selectedType == \"field\") then tbl = fields end\n if (selectedType == \"check\") then tbl = checks end\n if (selectedType == \"decal\") then tbl = decals end\n tbl[selectedId][spl[2]] = value\n if (spl[2] == \"separateColors\") then\n updateCheckFontAndColor(selectedId)\n refreshEditPanel()\n else\n if (not compareString(spl[2], { \"locked\", \"fillFromDisabled\" })) then\n refresh()\n end\n end\n updateSave()\nend\n\nfunction onFieldDropdownSelected(ply, option, id)\n local spl = split(id, \"/\")\n local parameterID = spl[2]\n local shouldRefresh = false\n if (parameterID == \"align\") then\n shouldRefresh = true\n if (option == \"Auto\") then\n option = 1\n elseif (option == \"Left\") then\n option = 2\n elseif (option == \"Center\") then\n option = 3\n elseif (option == \"Right\") then\n option = 4\n elseif (option == \"Justified\") then\n option = 5\n end\n end\n fields[selectedId][parameterID] = option\n updateFieldFontAndColor(selectedId)\n updateSave()\n if (shouldRefresh) then\n -- I did my best to not refresh here, but it seems like editInput doesn't care about alignment\n refresh()\n end\nend\n\nfunction onCheckDropdownSelected(ply, option, id)\n local spl = split(id, \"/\")\n local parameterID = spl[2]\n if (parameterID == \"value\") then\n local value = 0\n if (option == \"Off\") then value = 1 elseif (option == \"On\") then value = 2 end\n local check = checks[selectedId]\n check.value[selectedArrayId] = value\n updateCheckNameContentAndTooltip(selectedId)\n updateCheckFontAndColor(selectedId)\n updateSave()\n elseif (parameterID == \"tooltip\") then\n checks[selectedId].tooltip = option\n updateSave()\n else\n checks[selectedId][parameterID] = option\n updateSave()\n refresh()\n end\nend\n\nfunction onDecalDropdownSelected(ply, option, id)\n local spl = split(id, \"/\")\n local parameterID = spl[2]\n if (parameterID == \"tooltip\") then\n decals[selectedId].tooltip = option\n updateSave()\n else\n decals[selectedId][parameterID] = option\n updateSave()\n refresh()\n end\nend\n\nfunction onColorButtonPressed(ply, value, id)\n local spl = split(id, \"/\")\n local parameterID = spl[2]\n local tbl = nil\n if (selectedType == \"field\") then tbl = fields end\n if (selectedType == \"check\") then tbl = checks end\n local startingColor = tbl[selectedId][parameterID]\n if (startingColor == nil) then\n if (parameterID:find(\"textColor\")) then\n startingColor = tbl[selectedId].textColor\n end\n end\n\n ply.showColorDialog(startingColor,\n function(color, player_color)\n tbl[selectedId][parameterID] = color\n if (selectedType == \"field\") then\n updateFieldFontAndColor(selectedId)\n elseif (selectedType == \"check\") then\n updateCheckFontAndColor(selectedId)\n end\n local c = \"rgba(\" .. color.r .. \",\" .. color.g .. \",\" .. color.b .. \",1)\"\n local c2 = \"rgba(\" .. (color.r * 0.5 + 0.2) .. \",\" .. (color.g * 0.5 + 0.2) .. \",\" ..\n (color.b * 0.5 + 0.2) .. \",1)\"\n UI.setAttribute(id, \"colors\", c .. \"|\" .. c2 .. \"|\" .. c2 .. \"|\" .. c)\n UI.setAttribute(id .. \"/a\", \"percentage\", color.a * 100)\n updateSave()\n end\n )\nend\n\nfunction refreshAllPositionsAndSize()\n for k, v in pairs(fields) do\n updateFieldPositionAndSize(k)\n end\n for k, v in pairs(checks) do\n updateCheckPositionAndSize(k)\n end\n for k, v in pairs(decals) do\n updateDecalPositionAndSize(k)\n end\nend\n\nfunction onScaleChangedX(ply, value)\n scale.x = value\n updateSave()\n refreshAllPositionsAndSize()\nend\n\nfunction onScaleChangedY(ply, value)\n scale.y = value\n updateSave()\n refreshAllPositionsAndSize()\nend\n\nfunction onHeightChanged(ply, value)\n height = value\n updateSave()\n refreshAllPositionsAndSize()\nend\n\nfunction onFlip(ply, value)\n flip = value\n updateSave()\n refreshAllPositionsAndSize()\n local label = \"┗\"\n if (flip == \"True\") then label = \"┓\" end\n self.editButton({ index = 0, label = label })\n label = \"┏\"\n if (flip == \"True\") then label = \"┛\" end\n self.editButton({ index = 1, label = label })\n label = \"┛\"\n if (flip == \"True\") then label = \"┏\" end\n self.editButton({ index = 2, label = label })\n label = \"┓\"\n if (flip == \"True\") then label = \"┗\" end\n self.editButton({ index = 3, label = label })\nend\n\nfunction onCheckToggleChanged(ply, value, id)\n local spl = split(id, \"/\")\n local checkID = tonumber(spl[1])\n checks[checkID][spl[2]] = value\n\n updateSave()\n refresh()\nend\n\nfunction onDecalToggleChanged(ply, value, id)\n local spl = split(id, \"/\")\n local decalID = tonumber(spl[1])\n decals[decalID][spl[2]] = value\n\n updateSave()\n refresh()\nend\n\nfunction getDeselectButton(panelID)\n if (selectedId \u003e 0) then\n return [[\u003cRow preferredHeight=\"50\"\u003e\u003cButton onClick=\"]] ..\n self.getGUID() .. [[/deselect\"\u003eDeselect\u003c/Button\u003e\u003c/Row\u003e]]\n end\n return \"\"\nend\n\nfunction deselect()\n selectedId = 0\n selectedType = \"\"\n refresh()\nend\n\nfunction correctCheckboxesAndDecals()\n for decalID, decal in pairs(decals) do\n decal.pos.x = decal.pos.x / scale.x\n decal.pos.y = decal.pos.y / scale.y\n end\n for checkID, check in pairs(checks) do\n check.pos.x = check.pos.x / scale.x\n check.pos.y = check.pos.y / scale.y\n check.distance.x = check.distance.x / scale.x\n check.distance.y = check.distance.y / scale.y\n end\n refresh()\nend\n\nfunction attrId(str)\n return \"MarumEditorSheetAttribute_\" .. str\nend\n\nfunction getFieldPanel()\n local guid = self.getGUID()\n return [[\n \u003cPanel color=\"#282828\" id=\"]] .. attrId(\"FieldPanel\") .. [[\" active=\"False\" padding=\"12 12 4 4\"\u003e\n \u003cVerticalLayout\u003e\n \u003cTableLayout preferredHeight=\"60\" cellBackgroundColor=\"#00000000\" padding=\"0 0 4 30\" cellSpacing=\"5\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\" id=\"]] .. attrId(\"Field/ID\") .. [[\"\u003eText ?\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton id=\"]] ..\n attrId(\"Field/duplicate\") .. [[\" onClick=\"]] .. guid .. [[/duplicateField\"\u003eDuplicate\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton id=\"]] ..\n attrId(\"Field/delete\") .. [[\" onClick=\"]] .. guid .. [[/deleteField\"\u003eDelete\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003cHorizontalLayout preferredHeight=\"450\" spacing=\"50\"\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\" cellPadding=\"0 0 4 4\"\u003e\n\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"3\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eText\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eName\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/name\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" placeholder=\"Title\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"75\"\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eContent\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField tooltipPosition=\"Above\" lineType=\"MultiLineNewLine\" id=\"]] ..\n attrId(\"Field/content\") ..\n [[\" onEndEdit=\"]] .. guid .. [[/onValueEdited\" textColor=\"#000000\" placeholder=\"Text content\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eTooltip\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cDropdown id=\"]] ..\n attrId(\"Field/tooltip\") .. [[\" onValueChanged=\"]] .. guid .. [[/onFieldDropdownSelected\"\u003e\n \u003cOption\u003eNone\u003c/Option\u003e\n \u003cOption\u003eUse name as tooltip\u003c/Option\u003e\n \u003cOption\u003eUse content as tooltip\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eOn edit\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cDropdown id=\"]] ..\n attrId(\"Field/role\") .. [[\" onValueChanged=\"]] .. guid .. [[/onFieldDropdownSelected\"\u003e\n \u003cOption\u003eNothing\u003c/Option\u003e\n \u003cOption\u003eSet object's name\u003c/Option\u003e\n \u003cOption\u003eSet object's description\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"40\"\u003e\u003c/Row\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"3\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eFont and color\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eFont size\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/font\") ..\n [[\" onEndEdit=\"]] .. guid .. [[/onValueEdited\" textColor=\"#000000\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eAlign\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cDropdown id=\"]] ..\n attrId(\"Field/align\") .. [[\" tooltip=\"Field's tooltip\" onValueChanged=\"]] .. guid .. [[/onFieldDropdownSelected\"\u003e\n \u003cOption\u003eLeft\u003c/Option\u003e\n \u003cOption\u003eCenter\u003c/Option\u003e\n \u003cOption\u003eRight\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eText color\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cVerticalLayout\u003e\n \u003cButton preferredHeight=\"400\" id=\"]] ..\n attrId(\"Field/textColor\") .. [[\" onClick=\"]] .. guid .. [[/onColorButtonPressed\"\u003e\u003c/Button\u003e\n \u003cProgressBar id=\"]] ..\n attrId(\"Field/textColor/a\") ..\n [[\" color=\"#000000\" fillImageColor=\"#FFFFFF\" showPercentageText=\"False\"\u003e\u003c/ProgressBar\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eBackground Color\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cVerticalLayout\u003e\n \u003cButton preferredHeight=\"400\" id=\"]] ..\n attrId(\"Field/fieldColor\") .. [[\" onClick=\"]] .. guid .. [[/onColorButtonPressed\"\u003e\u003c/Button\u003e\n \u003cProgressBar id=\"]] ..\n attrId(\"Field/fieldColor/a\") ..\n [[\" color=\"#000000\" fillImageColor=\"#FFFFFF\" showPercentageText=\"False\"\u003e\u003c/ProgressBar\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n\n \u003cTableLayout cellBackgroundColor=\"#00000000\" cellPadding=\"0 0 4 4\"\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003ePosition and Size\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow preferredHeight=\"100\"\u003e\n \u003cCell columnSpan=\"6\"\u003e\n \u003cHorizontalLayout\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"4\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eNudge\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"4\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field_nudge\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onNudgeChanged\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Nudge distance\" characterValidation=\"Decimal\"\u003e]] ..\n nudgeDistance .. [[\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet1\"\u003e1\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet01\"\u003e0.1\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet001\"\u003e0.01\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet0001\"\u003e.001\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Field/nudgeUp\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeUp\" tooltip=\"Nudge up\" tooltipPosition=\"Above\"\u003e▲\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Field/nudgeLeft\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeLeft\" tooltip=\"Nudge left\" tooltipPosition=\"Left\"\u003e◀\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Field/nudgeRight\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeRight\" tooltip=\"Nudge right\" tooltipPosition=\"Right\"\u003e▶\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Field/nudgeDown\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeDown\" tooltip=\"Nudge down\" tooltipPosition=\"Below\"\u003e▼\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003ePos\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/pos/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"X\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/pos/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Y\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eSize\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/size/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Width\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/size/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Height\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"40\"\u003e\u003c/Row\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eArray\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eColumns/Rows\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/array/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Columns\" characterValidation=\"Integer\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/array/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Rows\" characterValidation=\"Integer\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eSpacing\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/distance/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Horizontal\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Field/distance/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Vertical\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"40\"\u003e\u003c/Row\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eToggles\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cToggle id=\"]] ..\n attrId(\"Field/counter\") ..\n [[\" textColor=\"#ffffff\" tooltipPosition=\"Below\" tooltip=\"Adds buttons to increase or decrease the value by one\" tooltipBackground=\"#000000\" isOn=\"False\" onValueChanged=\"]] ..\n guid .. [[/onToggleChanged\"\u003eCounter\u003c/Toggle\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cToggle id=\"]] ..\n attrId(\"Field/vsum\") ..\n [[\" textColor=\"#ffffff\" tooltipPosition=\"Below\" tooltip=\"Add an extra row that sums up the column's total\" tooltipBackground=\"#000000\" isOn=\"False\" onValueChanged=\"]] ..\n guid .. [[/onToggleChanged\"\u003eTotal sum\u003c/Toggle\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cToggle id=\"]] ..\n attrId(\"Field/locked\") ..\n [[\" textColor=\"#ffffff\" tooltip=\"Prevents users from changing this text\" tooltipBackground=\"#000000\" isOn=\"False\" onValueChanged=\"]] ..\n guid .. [[/onToggleChanged\"\u003eLock\u003c/Toggle\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Panel\u003e\n ]]\nend\n\nfunction getCheckPanel()\n local guid = self.getGUID()\n return [[\n \u003cPanel color=\"#282828\" id=\"]] .. attrId(\"CheckPanel\") .. [[\" active=\"False\" padding=\"12 12 4 4\"\u003e\n \u003cVerticalLayout\u003e\n \u003cTableLayout preferredHeight=\"60\" cellBackgroundColor=\"#00000000\" padding=\"0 0 4 30\" cellSpacing=\"5\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\" id=\"]] .. attrId(\"Check/ID\") ..\n [[\"\u003eCheckbox ?\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton id=\"]] ..\n attrId(\"Check/duplicate\") .. [[\" onClick=\"]] .. guid .. [[/duplicateCheck\"\u003eDuplicate\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton id=\"]] ..\n attrId(\"Check/delete\") .. [[\" onClick=\"]] .. guid .. [[/deleteCheck\"\u003eDelete\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003cHorizontalLayout preferredHeight=\"450\" spacing=\"50\"\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\" cellPadding=\"0 0 2 2\"\u003e\n\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"3\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eCheckbox\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eName\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/name\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" placeholder=\"Title\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eState\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cDropdown id=\"]] ..\n attrId(\"Check/value\") .. [[\" onValueChanged=\"]] .. guid .. [[/onCheckDropdownSelected\"\u003e\n \u003cOption\u003eOff\u003c/Option\u003e\n \u003cOption\u003eOn\u003c/Option\u003e\n \u003cOption\u003eDisabled\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eTooltip\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cDropdown id=\"]] ..\n attrId(\"Check/tooltip\") .. [[\" onValueChanged=\"]] .. guid .. [[/onCheckDropdownSelected\"\u003e\n \u003cOption\u003eNone\u003c/Option\u003e\n \u003cOption\u003eUse name as tooltip\u003c/Option\u003e\n \u003cOption\u003eShow controls hint as tooltip\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"65\"\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\" cellPadding=\"2 0 4 0\"\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleRight\" color=\"#ffffff\"\u003eOff\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/characters/empty\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onCheckValueEdited\" resizeTextForBestFit=\"True\" resizeTextMaxSize=\"35\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" placeholder=\" \"\u003e \u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleRight\" color=\"#ffffff\"\u003eOn\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/characters/filled\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onCheckValueEdited\" resizeTextForBestFit=\"True\" resizeTextMaxSize=\"35\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" placeholder=\" \"\u003e \u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleRight\" color=\"#ffffff\"\u003eDisabled\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/characters/disabled\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onCheckValueEdited\" resizeTextForBestFit=\"True\" resizeTextMaxSize=\"35\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" placeholder=\" \"\u003e \u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cButton onClick=\"]] .. guid .. [[/onCheckPresetButton\"\u003eCheckbox presets\u003c/Button\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eFont size\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/font\") ..\n [[\" onEndEdit=\"]] .. guid .. [[/onValueEdited\" textColor=\"#000000\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow preferredHeight=\"25\"\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cToggle id=\"]] ..\n attrId(\"Check/separateColors\") ..\n [[\" textColor=\"#ffffff\" tooltipPosition=\"Above\" tooltipBackgroundColor=\"#000000\" tooltip=\"Use a different color for the On, Off and Disabled states\" isOn=\"False\" onValueChanged=\"]] ..\n guid .. [[/onToggleChanged\"\u003eSeparate colors\u003c/Toggle\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow id=\"]] .. attrId(\"CheckOnColorRow\") .. [[\"\u003e\n \u003cCell\u003e\n \u003cText id=\"]] ..\n attrId(\"CheckOnColorLabel\") .. [[\" alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eOn color\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cVerticalLayout\u003e\n \u003cButton preferredHeight=\"400\" id=\"]] ..\n attrId(\"Check/textColorOn\") .. [[\" onClick=\"]] .. guid .. [[/onColorButtonPressed\"\u003e\u003c/Button\u003e\n \u003cProgressBar id=\"]] ..\n attrId(\"Check/textColorOn/a\") ..\n [[\" color=\"#000000\" fillImageColor=\"#FFFFFF\" showPercentageText=\"False\"\u003e\u003c/ProgressBar\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow id=\"]] .. attrId(\"CheckOffColorRow\") .. [[\"\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eOff color\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cVerticalLayout\u003e\n \u003cButton preferredHeight=\"400\" id=\"]] ..\n attrId(\"Check/textColorOff\") .. [[\" onClick=\"]] .. guid .. [[/onColorButtonPressed\"\u003e\u003c/Button\u003e\n \u003cProgressBar id=\"]] ..\n attrId(\"Check/textColorOff/a\") ..\n [[\" color=\"#000000\" fillImageColor=\"#FFFFFF\" showPercentageText=\"False\"\u003e\u003c/ProgressBar\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow id=\"]] .. attrId(\"CheckDisabledColorRow\") .. [[\"\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eDisabled color\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cVerticalLayout\u003e\n \u003cButton preferredHeight=\"400\" id=\"]] ..\n attrId(\"Check/textColorDisabled\") .. [[\" onClick=\"]] .. guid .. [[/onColorButtonPressed\"\u003e\u003c/Button\u003e\n \u003cProgressBar id=\"]] ..\n attrId(\"Check/textColorDisabled/a\") ..\n [[\" color=\"#000000\" fillImageColor=\"#FFFFFF\" showPercentageText=\"False\"\u003e\u003c/ProgressBar\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eBackground Color\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cVerticalLayout\u003e\n \u003cButton preferredHeight=\"400\" id=\"]] ..\n attrId(\"Check/checkColor\") .. [[\" onClick=\"]] .. guid .. [[/onColorButtonPressed\"\u003e\u003c/Button\u003e\n \u003cProgressBar id=\"]] ..\n attrId(\"Check/checkColor/a\") ..\n [[\" color=\"#000000\" fillImageColor=\"#FFFFFF\" showPercentageText=\"False\"\u003e\u003c/ProgressBar\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow id=\"]] .. attrId(\"CheckSeparateColorsSpacer1\") .. [[\"\u003e\u003c/Row\u003e\n \u003cRow id=\"]] .. attrId(\"CheckSeparateColorsSpacer2\") .. [[\"\u003e\u003c/Row\u003e\n \u003c/TableLayout\u003e\n\n \u003cTableLayout cellBackgroundColor=\"#00000000\" cellPadding=\"0 0 4 4\"\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003ePosition and Scale\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow preferredHeight=\"100\"\u003e\n \u003cCell columnSpan=\"6\"\u003e\n \u003cHorizontalLayout\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"4\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eNudge\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"4\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check_nudge\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onNudgeChanged\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Nudge distance\" characterValidation=\"Decimal\"\u003e]] ..\n nudgeDistance .. [[\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet1\"\u003e1\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet01\"\u003e0.1\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet001\"\u003e0.01\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet0001\"\u003e.001\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Check/nudgeUp\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeCheckUp\" tooltip=\"Nudge up\" tooltipPosition=\"Above\"\u003e▲\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Check/nudgeLeft\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeCheckLeft\" tooltip=\"Nudge left\" tooltipPosition=\"Left\"\u003e◀\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Check/nudgeRight\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeCheckRight\" tooltip=\"Nudge right\" tooltipPosition=\"Right\"\u003e▶\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Check/nudgeDown\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeCheckDown\" tooltip=\"Nudge down\" tooltipPosition=\"Below\"\u003e▼\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003ePos\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/pos/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"X\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/pos/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Y\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eScale\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/size/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Width\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/size/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Height\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"40\"\u003e\u003c/Row\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eArray\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eColumns/Rows\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/array/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Columns\" characterValidation=\"Integer\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/array/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Rows\" characterValidation=\"Integer\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eSpacing\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/distance/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Horizontal\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Check/distance/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Vertical\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"40\"\u003e\u003c/Row\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eToggles\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cToggle id=\"]] ..\n attrId(\"Check/fillFromDisabled\") ..\n [[\" textColor=\"#ffffff\" tooltipBackground=\"#000000\" tooltipPosition=\"Below\" tooltip=\"Makes it so that you can left click on this checkbox to fill it, even if it is disabled\" isOn=\"False\" onValueChanged=\"]] ..\n guid .. [[/onToggleChanged\"\u003eCan fill even if disabled\u003c/Toggle\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cToggle id=\"]] ..\n attrId(\"Check/locked\") ..\n [[\" textColor=\"#ffffff\" tooltipBackground=\"#000000\" tooltip=\"Prevents users from toggling this checkbox\" isOn=\"False\" onValueChanged=\"]] ..\n guid .. [[/onToggleChanged\"\u003eLock\u003c/Toggle\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Panel\u003e\n ]]\nend\n\nfunction getDecalPanel()\n local guid = self.getGUID()\n return [[\n \u003cPanel color=\"#282828\" id=\"]] .. attrId(\"DecalPanel\") .. [[\" active=\"False\" padding=\"12 12 4 4\"\u003e\n \u003cVerticalLayout\u003e\n \u003cTableLayout preferredHeight=\"60\" cellBackgroundColor=\"#00000000\" padding=\"0 0 4 30\" cellSpacing=\"5\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\" id=\"]] .. attrId(\"Decal/ID\") .. [[\"\u003eImage ?\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton id=\"]] ..\n attrId(\"Decal/duplicate\") .. [[\" onClick=\"]] .. guid .. [[/duplicateDecal\"\u003eDuplicate\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton id=\"]] ..\n attrId(\"Decal/delete\") .. [[\" onClick=\"]] .. guid .. [[/deleteDecal\"\u003eDelete\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003cHorizontalLayout preferredHeight=\"450\" spacing=\"50\"\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\" cellPadding=\"0 0 4 4\"\u003e\n\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"3\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eImage\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eName\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField tooltipPosition=\"Above\" tooltipBackgroundColor=\"#000000\" id=\"]] ..\n attrId(\"Decal/name\") ..\n [[\" onEndEdit=\"]] .. guid .. [[/onValueEdited\" textColor=\"#000000\" placeholder=\"Image URL\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eURL\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField tooltipPosition=\"Above\" tooltipBackgroundColor=\"#000000\" id=\"]] ..\n attrId(\"Decal/url\") ..\n [[\" onEndEdit=\"]] .. guid .. [[/onValueEdited\" textColor=\"#000000\" placeholder=\"Image URL\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eTooltip\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cDropdown id=\"]] ..\n attrId(\"Decal/tooltip\") .. [[\" onValueChanged=\"]] .. guid .. [[/onDecalDropdownSelected\"\u003e\n \u003cOption\u003eNone\u003c/Option\u003e\n \u003cOption\u003eUse name as tooltip\u003c/Option\u003e\n \u003cOption\u003eShow controls hint as tooltip\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"300\"\u003e\u003c/Row\u003e\n \u003c/TableLayout\u003e\n\n \u003cTableLayout cellBackgroundColor=\"#00000000\" cellPadding=\"0 0 4 4\"\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003ePosition and Scale\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow preferredHeight=\"100\"\u003e\n \u003cCell columnSpan=\"6\"\u003e\n \u003cHorizontalLayout\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"4\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eNudge\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"4\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Decal_nudge\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onNudgeChanged\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Nudge distance\" characterValidation=\"Decimal\"\u003e]] ..\n nudgeDistance .. [[\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet1\"\u003e1\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet01\"\u003e0.1\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet001\"\u003e0.01\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"]] .. guid .. [[/nudgeSet0001\"\u003e.001\u003c/Button\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003cTableLayout cellBackgroundColor=\"#00000000\"\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Decal/nudgeUp\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeDecalUp\" tooltip=\"Nudge up\" tooltipPosition=\"Above\"\u003e▲\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Decal/nudgeLeft\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeDecalLeft\" tooltip=\"Nudge left\" tooltipPosition=\"Left\"\u003e◀\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Decal/nudgeRight\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeDecalRight\" tooltip=\"Nudge right\" tooltipPosition=\"Right\"\u003e▶\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003cButton id=\"]] ..\n attrId(\"Decal/nudgeDown\") ..\n [[\" onClick=\"]] .. guid .. [[/nudgeDecalDown\" tooltip=\"Nudge down\" tooltipPosition=\"Below\"\u003e▼\u003c/Button\u003e\u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003ePos\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Decal/pos/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"X\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Decal/pos/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Y\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eScale\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Decal/scale/x\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Width\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Decal/scale/y\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Height\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"2\"\u003e\n \u003cText alignment=\"MiddleLeft\" color=\"#ffffff\"\u003eRotation\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell columnSpan=\"4\"\u003e\n \u003cInputField id=\"]] ..\n attrId(\"Decal/rotation\") ..\n [[\" onEndEdit=\"]] ..\n guid ..\n [[/onValueEdited\" textColor=\"#000000\" tooltipBackgroundColor=\"#000000\" tooltipPosition=\"Above\" tooltip=\"Height\" characterValidation=\"Decimal\"\u003e\u003c/InputField\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003cRow preferredHeight=\"40\"\u003e\u003c/Row\u003e\n \u003cRow preferredHeight=\"24\"\u003e\n \u003cCell columnSpan=\"6\" dontUseTableCellBackground=\"True\" color=\"#555555\"\u003e\n \u003cText alignment=\"MiddleCenter\" color=\"#ffffff\"\u003eToggles\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell columnSpan=\"3\"\u003e\n \u003cToggle id=\"]] ..\n attrId(\"Decal/locked\") ..\n [[\" textColor=\"#ffffff\" tooltipBackground=\"#000000\" tooltip=\"Prevents users from interacting with this image\" isOn=\"False\" onValueChanged=\"]] ..\n guid .. [[/onToggleChanged\"\u003eLock\u003c/Toggle\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow preferredHeight=\"90\"\u003e\u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/VerticalLayout\u003e\n \u003c/Panel\u003e\n ]]\nend\n\nfunction onCheckPresetButton(ply, value, id)\n local pres = { \"◌ ○ ●\", \" ◇ ◆\", \" □ ■\", \"/ △ ▴\", \" ◎ ◉\", \"- x\", \" ◾ ✦\", \" x ♥\" }\n ply.showOptionsDialog(\"Select checkbox preset\", pres, 1,\n function(text, index, player_color)\n local disabled = pres[index]:sub(1, 1)\n local empty = pres[index]:sub(3, 3)\n local filled = pres[index]:sub(5, 5)\n checks[selectedId].characters.disabled = disabled\n checks[selectedId].characters.empty = empty\n checks[selectedId].characters.filled = filled\n updateCheckNameContentAndTooltip(selectedId)\n refreshEditPanel()\n updateSave()\n end)\nend\n\nfunction nudgeSet1() updateNudgeDistance(1) end\n\nfunction nudgeSet01() updateNudgeDistance(0.1) end\n\nfunction nudgeSet001() updateNudgeDistance(0.01) end\n\nfunction nudgeSet0001() updateNudgeDistance(0.001) end\n\nfunction updateNudgeDistance(value)\n nudgeDistance = value\n UI.setAttribute(attrId(\"Field_nudge\"), \"text\", nudgeDistance)\n UI.setAttribute(attrId(\"Check_nudge\"), \"text\", nudgeDistance)\n UI.setAttribute(attrId(\"Decal_nudge\"), \"text\", nudgeDistance)\n updateSave()\nend\n\nfunction nudgeLeft(obj, value, id)\n local newX = fields[selectedId].pos.x - tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Field/pos/x\"), \"text\", newX)\n fields[selectedId].pos.x = newX\n updateFieldPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeUp(obj, value, id)\n local newY = fields[selectedId].pos.y - tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Field/pos/y\"), \"text\", newY)\n fields[selectedId].pos.y = newY\n updateFieldPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeRight(obj, value, id)\n local newX = fields[selectedId].pos.x + tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Field/pos/x\"), \"text\", newX)\n fields[selectedId].pos.x = newX\n updateFieldPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeDown(obj, value, id)\n local newY = fields[selectedId].pos.y + tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Field/pos/y\"), \"text\", newY)\n fields[selectedId].pos.y = newY\n updateFieldPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeDecalLeft(obj, value, id)\n local newX = decals[selectedId].pos.x - tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Decal/pos/x\"), \"text\", newX)\n decals[selectedId].pos.x = newX\n updateDecalPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeDecalUp(obj, value, id)\n local newY = decals[tonumber(selectedId)].pos.y - tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Decal/pos/y\"), \"text\", newY)\n decals[tonumber(selectedId)].pos.y = newY\n updateDecalPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeDecalRight(obj, value, id)\n local newX = decals[tonumber(selectedId)].pos.x + tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Decal/pos/x\"), \"text\", newX)\n decals[tonumber(selectedId)].pos.x = newX\n updateDecalPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeDecalDown(obj, value, id)\n local newY = decals[tonumber(selectedId)].pos.y + tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Decal/pos/y\"), \"text\", newY)\n decals[tonumber(selectedId)].pos.y = newY\n updateDecalPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeCheckLeft(obj, value, id)\n local newX = checks[tonumber(selectedId)].pos.x - tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Check/pos/x\"), \"text\", newX)\n checks[tonumber(selectedId)].pos.x = newX\n updateCheckPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeCheckUp(obj, value, id)\n local newY = checks[tonumber(selectedId)].pos.y - tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Check/pos/y\"), \"text\", newY)\n checks[tonumber(selectedId)].pos.y = newY\n updateCheckPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeCheckRight(obj, value, id)\n local newX = checks[tonumber(selectedId)].pos.x + tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Check/pos/x\"), \"text\", newX)\n checks[tonumber(selectedId)].pos.x = newX\n updateCheckPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction nudgeCheckDown(obj, value, id)\n local newY = checks[tonumber(selectedId)].pos.y + tonumber(nudgeDistance)\n UI.setAttribute(attrId(\"Check/pos/y\"), \"text\", newY)\n checks[tonumber(selectedId)].pos.y = newY\n updateCheckPositionAndSize(selectedId)\n updateSave()\nend\n\nfunction showEmptyPrompt()\n selectedId = 0\n selectedType = \"\"\n refreshEditPanel()\nend\n\nfunction duplicateField(ply, value, id)\n broadcastToColor(\"Text duplicated\", ply.color)\n local newField = JSON.decode(JSON.encode(fields[tonumber(selectedId)]))\n table.insert(fields, newField)\n updateSave()\n selectField({ id = #fields, arrayId = 1 })\n refresh()\nend\n\nfunction deleteField(ply, value, id)\n broadcastToColor(\"Text deleted\", ply.color)\n table.remove(fields, tonumber(selectedId))\n updateSave()\n if (#fields \u003e 0) then\n selectField({ id = math.max(selectedId - 1, 1), arrayId = 1 })\n else\n if (#checks \u003e 0) then\n selectCheck({ id = 1, arrayId = 1 })\n elseif (#decals \u003e 0) then\n selectDecal(1)\n else\n showEmptyPrompt()\n end\n end\n refresh()\nend\n\nfunction duplicateCheck(ply, value, id)\n broadcastToColor(\"Check duplicated\", ply.color)\n local newCheck = JSON.decode(JSON.encode(checks[tonumber(selectedId)]))\n table.insert(checks, newCheck)\n updateSave()\n selectCheck({ id = #checks, arrayId = 1 })\n refresh()\nend\n\nfunction deleteCheck(ply, value, id)\n broadcastToColor(\"Check deleted\", ply.color)\n table.remove(checks, tonumber(selectedId))\n updateSave()\n if (#checks \u003e 0) then\n selectCheck({ id = math.max(selectedId - 1, 1), arrayId = 1 })\n else\n if (#fields \u003e 0) then\n selectField({ id = 1, arrayId = 1 })\n elseif (#decals \u003e 0) then\n selectDecal(1)\n else\n showEmptyPrompt()\n end\n end\n refresh()\nend\n\nfunction duplicateDecal(ply, value, id)\n broadcastToColor(\"Image duplicated\", ply.color)\n local newDecal = JSON.decode(JSON.encode(decals[tonumber(selectedId)]))\n table.insert(decals, newDecal)\n updateSave()\n selectDecal(#decals)\n refresh()\nend\n\nfunction deleteDecal(ply, value, id)\n broadcastToColor(\"Image deleted\", ply.color)\n table.remove(decals, tonumber(selectedId))\n updateSave()\n if (#decals \u003e 0) then\n selectDecal(math.max(selectedId - 1, 1))\n else\n if (#fields \u003e 0) then\n selectField({ id = 1, arrayId = 1 })\n elseif (#checks \u003e 0) then\n selectCheck({ id = 1, arrayId = 1 })\n else\n showEmptyPrompt()\n end\n end\n refresh()\nend\n\nfunction addField(obj, player_clicker_color, alt_click)\n local newField = {\n value = { \"?\" },\n name = \"\",\n tooltip = \"name\",\n role = \"Normal Field\",\n textColor = { r = 0, g = 0, b = 0, a = 1 },\n fieldColor = { r = 1, g = 1, b = 1, a = 1 },\n font = 150,\n align = 3,\n pos = { x = 0, y = 0 },\n size = { x = 250, y = 250 },\n array = { x = 1, y = 1 },\n distance = { x = 1, y = 1 },\n locked = false\n }\n table.insert(fields, newField)\n selectField({ id = #fields, arrayId = 1 })\n updateSave()\n refresh()\nend\n\nfunction addCheck(obj, player_clicker_color, alt_click)\n local newCheck = {\n name = \"\",\n tooltip = \"hint\",\n value = { 1 },\n characters = { disabled = \"◌\", empty = \"○\", filled = \"●\" },\n textColor = { r = 0, g = 0, b = 0, a = 1 },\n textColorOff = { r = 0, g = 0, b = 0, a = 1 },\n textColorDisabled = { r = 0.5, g = 0.5, b = 0.5, a = 1 },\n separateColors = false,\n fillFromDisabled = false,\n checkColor = { r = 1, g = 1, b = 1, a = 1 },\n pos = { x = 0, y = 0 },\n size = { x = 1, y = 1 },\n array = { x = 1, y = 1 },\n distance = { x = 1, y = 1 },\n font = 500,\n locked = false\n }\n table.insert(checks, newCheck)\n selectCheck({ id = #checks, arrayId = 1 })\n updateSave()\n refresh()\nend\n\nfunction addDecal(obj, player_clicker_color, alt_click)\n local newDecal = {\n name = \"\",\n url = \"https://api.tabletopsimulator.com/img/TSIcon.png\",\n tooltip = \"name\",\n pos = { x = 0, y = 0 },\n rotation = 0,\n scale = { x = 1, y = 1 },\n locked = false\n }\n table.insert(decals, newDecal)\n selectDecal(#decals)\n updateSave()\n refresh()\nend\n\nfunction onDestroy()\n closePanel()\nend\n\nfunction cutAtWord(inputStr, delimiter)\n local t = {}\n local pattern = \"(.-)\" .. delimiter\n for str in inputStr:gmatch(pattern) do\n table.insert(t, str)\n end\n if (#t \u003e 0) then\n return t[1]\n else\n return false\n end\nend\n\nfunction waitForUiLoaded(callback)\n if UI.loading == false then\n callback()\n return nil\n end\n\n return Wait.condition(\n function()\n callback()\n end,\n function()\n return UI.loading == false\n end\n )\nend", + "LuaScriptState": "{\"checks\":[],\"decals\":[{\"locked\":false,\"name\":\"Arkham SCE logo\",\"pos\":{\"x\":3.1,\"y\":2.2},\"rotation\":0,\"scale\":{\"x\":\"2\",\"y\":\"2\"},\"tooltip\":\"None\",\"url\":\"http://cloud-3.steamusercontent.com/ugc/2501268517218943111/803E57A7B3E9765DF342050EE6C71D69473A7388/\"},{\"locked\":false,\"name\":\"Bootlegger Finn\",\"pos\":{\"x\":3.5,\"y\":-1.89},\"rotation\":\"25\",\"scale\":{\"x\":\"1\",\"y\":\"1\"},\"tooltip\":\"None\",\"url\":\"http://cloud-3.steamusercontent.com/ugc/2037357792052848566/5DA900C430E97D3DFF2C9B8A3DB1CB2271791FC7/\"},{\"locked\":false,\"name\":\"black bar\",\"pos\":{\"x\":0,\"y\":-2.7},\"rotation\":0,\"scale\":{\"x\":\"8\",\"y\":\"0.03\"},\"tooltip\":\"None\",\"url\":\"http://cloud-3.steamusercontent.com/ugc/2501268517219098388/0936FEE03B410319658B5E05DB5D486CEDDE98F5/\"}],\"fields\":[{\"align\":3,\"array\":{\"x\":\"1\",\"y\":\"1\"},\"counter\":\"False\",\"distance\":{\"x\":\"1\",\"y\":\"1\"},\"fieldColor\":{\"a\":0,\"b\":1,\"g\":1,\"r\":1},\"font\":\"200\",\"locked\":false,\"name\":\"Patch Notes\",\"pos\":{\"x\":\"0\",\"y\":-2.9},\"role\":\"Normal Field\",\"size\":{\"x\":\"3750\",\"y\":\"250\"},\"textColor\":{\"a\":1,\"b\":0,\"g\":0,\"r\":0},\"tooltip\":\"None\",\"value\":[\"Arkham Horror LCG SCE 3.9.0 - 07/08/2024\"]},{\"align\":2,\"array\":{\"x\":\"1\",\"y\":1},\"distance\":{\"x\":\"1\",\"y\":\"1\"},\"fieldColor\":{\"a\":0,\"b\":1,\"g\":1,\"r\":1},\"font\":\"89\",\"locked\":false,\"name\":\"Details\",\"pos\":{\"x\":\"0\",\"y\":0.4},\"role\":\"Nothing\",\"size\":{\"x\":\"3750\",\"y\":\"2750\"},\"textColor\":{\"a\":1,\"b\":0,\"g\":0,\"r\":0},\"tooltip\":\"None\",\"value\":[\"Thanks for downloading! We're happy to present you a rather big update this time :-)\\n\\nNew things\\n- updated note card for patch notes (bless Marum for his awesome tool!)\\n- automated discarding for Patrice\\n- confirmation dialog for discard hotkey (e.g. for locations)\\n- helpers for cards that redraw tokens and Kohaku\\n- displaying of token count for cards that seal tokens\\r\\n- new action / ability tokens (replacing the old ones)\\r\\n- option to enable all card helpers (e.g. Heavy Furs)\\r\\n- option to load class-colored playermat backgrounds\\n- coloring for player names in broadcasts\\n- right-click option for RBW button on Player Card Panel to specify trait(s)\\n\\nUpdates\\r\\n- performed a small clean up of the bottom corners of the table\\n- \\\"Numpad 9\\\" to rearranges present tokens (on top of adding a resource)\\n- Scroll of Secrets context menu helper now displays player names instead of colors\\r\\n- Player Card Panel can display fan-made cards with a new \\\"custom\\\" cycle button)\\n- updated Family Inheritance helper to a proper UI\\n- \\\"Discard object\\\" gamekey works for selected objects\\r\\n- updated a bunch of tools like Clean Up Helper, Drawing Tool,\\nHand Helper, Token Arranger and Search Assistant\\n\\nFixes\\r\\r\\n- Bugfix for attempting to draw an encounter card while there is no deck\\r\\n- Bugfix for Navigation Overlay: now checks if playmat is occupied\\r\\n- Bugfix for Phase Tracker broadcasting\\r\\n- Performance and file size improvements (e.g. by adding download\\nfunctions for CYOA campaign guides and Arkham Fantasy standees)\"]}],\"flip\":\"False\",\"height\":\"0.1\",\"locks\":{\"checks\":false,\"decals\":false,\"fields\":false},\"nudgeDistance\":0.1,\"scale\":{\"x\":\"0.3\",\"y\":\"0.3\"},\"sheetLocked\":true}", + "MeasureMovement": false, + "Name": "Custom_Tile", + "Nickname": "Patch Notes", + "Snap": true, + "Sticky": true, + "Tooltip": true, + "Transform": { + "posX": -27, + "posY": 1.481, + "posZ": -56.165, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 7.5, + "scaleY": 1, + "scaleZ": 7.5 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0, + "g": 0, + "r": 0 + }, "CustomPDF": { "PDFPage": 0, "PDFPageOffset": 0, @@ -8166,6 +8415,57 @@ "Value": 0, "XmlUI": "" }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0, + "g": 0, + "r": 0 + }, + "CustomPDF": { + "PDFPage": 0, + "PDFPageOffset": 0, + "PDFPassword": "", + "PDFUrl": "http://cloud-3.steamusercontent.com/ugc/2444972799649422837/EE123FE4A20C941BB74142001EA8F48FA67B68AC/" + }, + "Description": "", + "DragSelectable": true, + "GMNotes": "", + "GUID": "faqfaq", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": true, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "Custom_PDF", + "Nickname": "Latest FAQ", + "Snap": true, + "Sticky": true, + "Tooltip": true, + "Transform": { + "posX": -42, + "posY": 1.481, + "posZ": 71, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 4.5, + "scaleY": 1, + "scaleZ": 4.5 + }, + "Value": 0, + "XmlUI": "" + }, { "AltLookAngle": { "x": 0, @@ -17274,376 +17574,6 @@ "Value": 0, "XmlUI": "" }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "a": 0.25, - "b": 0.168, - "g": 0.701, - "r": 0.192 - }, - "Description": "", - "DragSelectable": true, - "FogColor": "Green", - "FogHidePointers": false, - "FogReverseHiding": false, - "FogSeethrough": true, - "GMNotes": "", - "GUID": "3aab97", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "FogOfWarTrigger", - "Nickname": "", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -21.648, - "posY": 0.87, - "posZ": 22.438, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 2.8, - "scaleY": 0.55, - "scaleZ": 3.8 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "CardID": 266400, - "ColorDiffuse": { - "b": 0.71324, - "g": 0.71324, - "r": 0.71324 - }, - "CustomDeck": { - "2664": { - "BackIsHidden": true, - "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", - "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2038486156990333452/327F5C791C48AF81F5EBCF5ED72211543E4DFB33/", - "NumHeight": 1, - "NumWidth": 1, - "Type": 0, - "UniqueBack": false - } - }, - "Description": "Ally. Detective. Police.", - "DragSelectable": true, - "GMNotes": "{\n \"id\": \"b5151\",\n \"type\": \"Asset\",\n \"class\": \"Guardian\",\n \"cost\": 3,\n \"level\": 3,\n \"traits\": \"Ally. Detective. Police.\",\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"cycle\": \"Beta\"\n}", - "GUID": "94f23b", - "Grid": true, - "GridProjection": false, - "Hands": true, - "HideWhenFaceDown": true, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "CardCustom", - "Nickname": "Alice Luxley (2)", - "SidewaysCard": false, - "Snap": true, - "Sticky": true, - "Tags": [ - "Asset", - "PlayerCard" - ], - "Tooltip": true, - "Transform": { - "posX": 0, - "posY": 0, - "posZ": -2.257, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "CardID": 266500, - "ColorDiffuse": { - "b": 0.71324, - "g": 0.71324, - "r": 0.71324 - }, - "CustomDeck": { - "2665": { - "BackIsHidden": true, - "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", - "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2038486156990333641/5C6B3E30DDCB25F7DA24B2B7C43688AA2AE4744E/", - "NumHeight": 1, - "NumWidth": 1, - "Type": 0, - "UniqueBack": false - } - }, - "Description": "Item. Weapon. Melee.", - "DragSelectable": true, - "GMNotes": "{\n \"id\": \"b8060\",\n \"type\": \"Asset\",\n \"class\": \"Guardian|Mystic\",\n \"cost\": 3,\n \"level\": 3,\n \"traits\": \"Item. Weapon. Melee.\",\n \"combatIcons\": 1,\n \"willpowerIcons\": 1,\n \"cycle\": \"Beta\"\n}", - "GUID": "a20aef", - "Grid": true, - "GridProjection": false, - "Hands": true, - "HideWhenFaceDown": true, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "CardCustom", - "Nickname": "Dragon Pole (3)", - "SidewaysCard": false, - "Snap": true, - "Sticky": true, - "Tags": [ - "Asset", - "PlayerCard" - ], - "Tooltip": true, - "Transform": { - "posX": 50.601, - "posY": 1.495, - "posZ": 36.996, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "CardID": 542300, - "ColorDiffuse": { - "b": 0.71324, - "g": 0.71324, - "r": 0.71324 - }, - "CustomDeck": { - "5423": { - "BackIsHidden": true, - "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", - "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2022727271907675521/7CD01B7199EDE77C9E62CC6D2EAFF53D99AF5BC5/", - "NumHeight": 1, - "NumWidth": 1, - "Type": 0, - "UniqueBack": false - } - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "{\n \"id\": \"B2023\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 1,\n \"level\": 3,\n \"traits\": \"Item. Clothing.\",\n \"agilityIcons\": 1,\n \"cycle\": \"Standalone\"\n}", - "GUID": "5cb973", - "Grid": true, - "GridProjection": false, - "Hands": true, - "HideWhenFaceDown": true, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "CardCustom", - "Nickname": "Fine Clothes (3)", - "SidewaysCard": false, - "Snap": true, - "Sticky": true, - "Tags": [ - "Asset", - "PlayerCard" - ], - "Tooltip": true, - "Transform": { - "posX": 8.972, - "posY": 5.01, - "posZ": -16.689, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "CardID": 542400, - "ColorDiffuse": { - "b": 0.71324, - "g": 0.71324, - "r": 0.71324 - }, - "CustomDeck": { - "5424": { - "BackIsHidden": true, - "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", - "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2022727271907674847/3938E37E1C41BA1E6F1DE628CE1D108E54C668EA/", - "NumHeight": 1, - "NumWidth": 1, - "Type": 0, - "UniqueBack": false - } - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "{\n \"id\": \"A2023\",\n \"type\": \"Asset\",\n \"class\": \"Mystic\",\n \"cost\": 2,\n \"level\": 2,\n \"traits\": \"Item. Relic. Weapon. Melee.\",\n \"combatIcons\": 1,\n \"cycle\": \"Standalone\"\n}", - "GUID": "9c32e2", - "Grid": true, - "GridProjection": false, - "Hands": true, - "HideWhenFaceDown": true, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "CardCustom", - "Nickname": "Sword Cane (2)", - "SidewaysCard": false, - "Snap": true, - "Sticky": true, - "Tags": [ - "Asset", - "PlayerCard" - ], - "Tooltip": true, - "Transform": { - "posX": 8.972, - "posY": 3.706, - "posZ": -16.689, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/1016065725028510450/7BC76948EE00979A428636EF40D46AE8634760A6/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/166Kdeqb", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "42cd6e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "Leaked Items", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -57.92, - "posY": 2.953, - "posZ": -82.991, - "rotX": 90, - "rotY": 135, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, { "AltLookAngle": { "x": 0, @@ -18843,7 +18773,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playmat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playmat\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n playmat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playmat.positionToLocal(playmat.getPosition())\n searchParam.pos = playmat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playmat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playmat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playermat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playermat\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n playermat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playermat.positionToLocal(playermat.getPosition())\n searchParam.pos = playermat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playermat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playermat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -18909,7 +18839,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playmat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playmat\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n playmat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playmat.positionToLocal(playmat.getPosition())\n searchParam.pos = playmat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playmat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playmat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playermat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playermat\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n playermat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playermat.positionToLocal(playermat.getPosition())\n searchParam.pos = playermat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playermat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playermat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -18975,7 +18905,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playmat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playmat\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n playmat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playmat.positionToLocal(playmat.getPosition())\n searchParam.pos = playmat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playmat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playmat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playermat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playermat\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n playermat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playermat.positionToLocal(playermat.getPosition())\n searchParam.pos = playermat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playermat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playermat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -19041,7 +18971,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playmat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playmat\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n playmat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playmat.positionToLocal(playmat.getPosition())\n searchParam.pos = playmat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playmat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playmat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/ClueCounter\")\nend)\n__bundle_register(\"playermat/ClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\nexposedValue = 0\n\nlocal playermat\nlocal searchParam = {}\n\nfunction onLoad()\n self.createButton({\n label = \"\",\n click_function = \"countItems\",\n function_owner = self,\n position = { 0, 0.1, 0 },\n height = 0,\n width = 0,\n font_color = { 0, 0, 0 },\n font_size = 2000\n })\n\n -- get closest playermat\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n playermat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n\n -- get search parameters (threat area excluded)\n local localPos = playermat.positionToLocal(playermat.getPosition())\n searchParam.pos = playermat.positionToWorld(localPos + Vector(0, 0, 0.4))\n searchParam.rot = playermat.getRotation() + Vector(0, 90, 0)\n searchParam.size = Vector(8, 1, 27)\n searchParam.filter = \"isClue\"\n\n -- start loop\n Wait.time(countItems, 1.5, -1)\nend\n\n-- activated once per second, counts clues on the playermat\nfunction countItems()\n local totalValue = 0\n for _, item in ipairs(getClues()) do\n totalValue = totalValue + math.abs(item.getQuantity())\n end\n exposedValue = totalValue\n self.editButton({ index = 0, label = totalValue })\nend\n\nfunction removeAllClues(trash)\n for _, obj in ipairs(getClues()) do\n trash.putObject(obj)\n end\nend\n\nfunction getClues()\n return searchLib.inArea(searchParam.pos, searchParam.rot, searchParam.size, searchParam.filter)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -19098,7 +19028,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/MasterClueCounter\")\nend)\n__bundle_register(\"core/MasterClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- variables are intentionally global to be accessible\ncount = 0\nuseClickableCounters = false\n\nfunction onSave() return JSON.encode(useClickableCounters) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n useClickableCounters = JSON.decode(savedData)\n end\n\n self.createButton({\n label = \"0\",\n click_function = \"removeAllPlayerClues\",\n tooltip = \"Click here to remove all collected clues\",\n function_owner = self,\n position = { 0, 0.06, 0 },\n height = 900,\n width = 900,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 650,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n Wait.time(sumClues, 2, -1)\nend\n\n-- removes all player clues by calling the respective function from the counting bowls / clickers\nfunction removeAllPlayerClues()\n printToAll(count .. \" clue(s) from playermats removed.\", \"White\")\n playmatApi.removeClues(\"All\")\n self.editButton({ index = 0, label = \"0\" })\nend\n\n-- gets the counted values from the counting bowls / clickers and sums them up\nfunction sumClues()\n count = playmatApi.getClueCount(useClickableCounters, \"All\")\n self.editButton({ index = 0, label = tostring(count) })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/MasterClueCounter\")\nend)\n__bundle_register(\"core/MasterClueCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- variables are intentionally global to be accessible\ncount = 0\nuseClickableCounters = false\n\nfunction onSave() return JSON.encode(useClickableCounters) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n useClickableCounters = JSON.decode(savedData)\n end\n\n self.createButton({\n label = \"0\",\n click_function = \"removeAllPlayerClues\",\n tooltip = \"Click here to remove all collected clues\",\n function_owner = self,\n position = { 0, 0.06, 0 },\n height = 900,\n width = 900,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 650,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n Wait.time(sumClues, 2, -1)\nend\n\n-- removes all player clues by calling the respective function from the counting bowls / clickers\nfunction removeAllPlayerClues()\n printToAll(count .. \" clue(s) from playermats removed.\", \"White\")\n playermatApi.removeClues(\"All\")\n self.editButton({ index = 0, label = \"0\" })\nend\n\n-- gets the counted values from the counting bowls / clickers and sums them up\nfunction sumClues()\n count = playermatApi.getClueCount(useClickableCounters, \"All\")\n self.editButton({ index = 0, label = tostring(count) })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "false", "MeasureMovement": false, "Name": "Custom_Token", @@ -19139,6 +19069,71 @@ "r": 1 }, "ContainedObjects": [ + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 1, + "g": 1, + "r": 1 + }, + "CustomMesh": { + "CastShadows": true, + "ColliderURL": "http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/", + "Convex": true, + "CustomShader": { + "FresnelStrength": 0, + "SpecularColor": { + "b": 0.34, + "g": 0.51, + "r": 0.72 + }, + "SpecularIntensity": 0.4, + "SpecularSharpness": 7 + }, + "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/254843371583187306/6844B833AD55B9A34095067B201B311E1348325F/", + "MaterialIndex": 2, + "MeshURL": "http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/", + "NormalURL": "", + "TypeIndex": 0 + }, + "Description": "Include this in custom content for clue spawning!", + "DragSelectable": true, + "GMNotes": "", + "GUID": "2547b3", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/CustomDataHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[\nKnown locations and clues. We check this to determine if we should\natttempt to spawn clues, first we look for \u003cLOCATION_NAME\u003e_\u003cGUID\u003e and if\nwe find nothing we look for \u003cLOCATION_NAME\u003e\nformat is [location_guid -\u003e clueCount]\n]]\nLOCATIONS_DATA_JSON = [[\n{\n \"San Francisco\": {\"type\": \"fixed\", \"value\": 1, \"clueSide\": \"back\"},\n \"\tArkham\": {\"type\": \"perPlayer\", \"value\": 1, \"clueSide\": \"back\"},\n \"Buenos Aires\": {\"type\": \"fixed\", \"value\": 2, \"clueSide\": \"back\"},\n \"\tLondon\": {\"type\": \"perPlayer\", \"value\": 2, \"clueSide\": \"front\"},\n \"Rome\": {\"type\": \"perPlayer\", \"value\": 3, \"clueSide\": \"front\"},\n \"Istanbul\": {\"type\": \"perPlayer\", \"value\": 4, \"clueSide\": \"front\"},\n \"Tokyo_123abc\": {\"type\": \"perPlayer\", \"value\": 0, \"clueSide\": \"back\"},\n \"Tokyo_456efg\": {\"type\": \"perPlayer\", \"value\": 4, \"clueSide\": \"back\"},\n \"Tokyo\": {\"type\": \"fixed\", \"value\": 2, \"clueSide\": \"back\"},\n \"Shanghai_123\": {\"type\": \"fixed\", \"value\": 12, \"clueSide\": \"front\"},\n \"Sydney\": {\"type\": \"fixed\", \"value\": 0, \"clueSide\": \"front\"}\n}\n]]\n\n\nPLAYER_CARD_DATA_JSON = [[\n{\n \"Tool Belt\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 2\n },\n \"Tool Belt (3)\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 4\n },\n \"Yithian Rifle\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 3\n },\n \"xxx\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 3\n }\n}\n]]\n\nHIDDEN_CARD_DATA = {\n \"Unpleasant Card (Doom)\",\n \"Unpleasant Card (Gloom)\",\n \"The Case of the Scarlet DOOOOOM!\"\n}\n\nLOCATIONS_DATA = JSON.decode(LOCATIONS_DATA_JSON)\nPLAYER_CARD_DATA = JSON.decode(PLAYER_CARD_DATA_JSON)\n\nfunction onload(save_state)\n local playArea = getObjectFromGUID('721ba2')\n playArea.call(\"updateLocations\", {self.getGUID()})\n local playerMatWhite = getObjectFromGUID('8b081b')\n playerMatWhite.call(\"updatePlayerCards\", {self.getGUID()})\n local playerMatOrange = getObjectFromGUID('bd0ff4')\n playerMatOrange.call(\"updatePlayerCards\", {self.getGUID()})\n local playerMatGreen = getObjectFromGUID('383d8b')\n playerMatGreen.call(\"updatePlayerCards\", {self.getGUID()})\n local playerMatRed = getObjectFromGUID('0840d5')\n playerMatRed.call(\"updatePlayerCards\", {self.getGUID()})\n local dataHelper = getObjectFromGUID('708279')\n dataHelper.call(\"updateHiddenCards\", {self.getGUID()})\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/CustomDataHelper\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "Custom_Model", + "Nickname": "Custom Data Helper", + "Snap": true, + "Sticky": true, + "Tooltip": true, + "Transform": { + "posX": 30.163, + "posY": 4.157, + "posZ": -21.518, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 0.5, + "scaleY": 0.5, + "scaleZ": 0.5 + }, + "Value": 0, + "XmlUI": "" + }, { "AltLookAngle": { "x": 0, @@ -19370,6 +19365,63 @@ }, "Value": 0, "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 1, + "g": 1, + "r": 1 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": false, + "Thickness": 0.1, + "Type": 1 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1634201755654309873/9A23829955A98CBAC1E6BA2D3E14D4FFF0A86463/", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1634201755654309427/59F903E0AF5599D782B756AB92B5D9203002DF61/", + "WidthScale": 0 + }, + "Description": "", + "DragSelectable": true, + "GMNotes": "", + "GUID": "bc81cb", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "Custom_Tile", + "Nickname": "Double-Sided Resource", + "Snap": false, + "Sticky": true, + "Tooltip": true, + "Transform": { + "posX": 29.9, + "posY": 4.095, + "posZ": -21.013, + "rotX": 0, + "rotY": 0, + "rotZ": 359, + "scaleX": 0.46, + "scaleY": 1, + "scaleZ": 0.46 + }, + "Value": 0, + "XmlUI": "" } ], "CustomMesh": { @@ -20389,7 +20441,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PlayArea\")\nend)\n__bundle_register(\"core/PlayArea\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- Location connection directional options\nlocal BIDIRECTIONAL = 0\nlocal ONE_WAY = 1\nlocal INCOMING_ONE_WAY = 2\n\n-- Connector draw parameters\nlocal CONNECTION_THICKNESS = 0.015\nlocal DRAGGING_CONNECTION_THICKNESS = 0.15\nlocal DRAGGING_CONNECTION_COLOR = { 0.8, 0.8, 0.8, 1 }\nlocal DIRECTIONAL_ARROW_DISTANCE = 3.5\nlocal ARROW_ARM_LENGTH = 0.9\nlocal ARROW_ANGLE = 25\n\n-- Height to draw the connector lines, places them just above the table and always below cards\nlocal CONNECTION_LINE_Y = 1.529\n\n-- used for recreating the link to a custom data helper after image change\ncustomDataHelper = nil\n\nlocal DEFAULT_URL = \"http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/\"\n\nlocal SHIFT_OFFSETS = {\n left = { x = 0.00, y = 0, z = 7.67 },\n right = { x = 0.00, y = 0, z = -7.67 },\n up = { x = 6.54, y = 0, z = 0.00 },\n down = { x = -6.54, y = 0, z = 0.00 }\n}\nlocal SHIFT_EXCLUSION = {\n [\"b7b45b\"] = true,\n [\"f182ee\"] = true,\n [\"721ba2\"] = true\n}\nlocal LOC_LINK_EXCLUDE_SCENARIOS = {\n [\"The Witching Hour\"] = true,\n [\"The Heart of Madness\"] = true\n}\n\nlocal locations = {}\nlocal locationConnections = {}\nlocal draggingGuids = {}\nlocal missingData = {}\nlocal collisionEnabled = false\nlocal currentScenario, connectionsEnabled\n\n---------------------------------------------------------\n-- general code\n---------------------------------------------------------\n\nfunction onSave()\n return JSON.encode({\n trackedLocations = locations,\n currentScenario = currentScenario,\n connectionColor = connectionColor,\n connectionsEnabled = connectionsEnabled\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData) or {}\n locations = loadedData.trackedLocations or {}\n currentScenario = loadedData.currentScenario\n connectionColor = loadedData.connectionColor or { 0.4, 0.4, 0.4, 1 }\n connectionsEnabled = loadedData.connectionsEnabled or true\n end\n\n -- this needs to be here since the playarea will be reloaded when the image changes\n self.interactable = false\n\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n-- data to the local token manager instance.\n---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\nfunction updateLocations(args)\n customDataHelper = getObjectFromGUID(args[1])\n if customDataHelper ~= nil then\n tokenManager.addLocationData(customDataHelper.getTable(\"LOCATIONS_DATA\"))\n end\nend\n\n---------------------------------------------------------\n-- TTS event handling\n---------------------------------------------------------\n\nfunction onCollisionEnter(collisionInfo)\n if not collisionEnabled then return end\n\n local object = collisionInfo.collision_object\n\n if object.type == \"Deck\" then\n table.insert(missingData, object)\n end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n -- check if we should spawn clues here and do so according to playercount\n if shouldSpawnTokens(object) then\n tokenManager.spawnForCard(object)\n end\n\n -- If this card was being dragged, clear the dragging connections. A multi-drag/drop may send\n -- the dropped card immediately into a deck, so this has to be done here\n if draggingGuids[object.getGUID()] ~= nil then\n object.setVectorLines({})\n draggingGuids[object.getGUID()] = nil\n end\n\n maybeTrackLocation(object)\nend\n\nfunction onCollisionExit(collisionInfo)\n maybeUntrackLocation(collisionInfo.collision_object)\nend\n\n-- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well\nfunction onObjectDestroy(object)\n maybeUntrackLocation(object)\nend\n\nfunction onObjectPickUp(_, object)\n if object.type ~= \"Card\" then return end\n\n -- onCollisionExit USUALLY fires first, so we have to check the card to see if it's a location we should be tracking\n if showLocationLinks() and isInPlayArea(object) and object.getGMNotes() ~= nil and object.getGMNotes() ~= \"\" then\n local pickedUpGuid = object.getGUID()\n local metadata = JSON.decode(object.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n -- onCollisionExit sometimes comes 1 frame after onObjectPickUp (rather than before it or in\n -- the same frame). This causes a mismatch in the data between dragging the on-table, and\n -- that one frame draws connectors on the card which then show up as shadows for snap points.\n -- Waiting ensures we always do thing in the expected Exit-\u003ePickUp order\n Wait.frames(function()\n if object.is_face_down then\n draggingGuids[pickedUpGuid] = metadata.locationBack\n else\n draggingGuids[pickedUpGuid] = metadata.locationFront\n end\n rebuildConnectionList()\n end, 2)\n end\n end\nend\n\n-- Due to the frequence of onUpdate calls, ensure that we only process any changes once\nfunction onUpdate()\n local needsConnectionRebuild = false\n local needsConnectionDraw = false\n for guid, _ in pairs(draggingGuids) do\n local obj = getObjectFromGUID(guid)\n if obj == nil or not isInPlayArea(obj) then\n draggingGuids[guid] = nil\n needsConnectionRebuild = true\n -- If object still exists then it's outside the area and needs to lose the lines attached to it\n if obj ~= nil then\n obj.setVectorLines({})\n end\n end\n -- Even if the last location left the play area, need one last draw to clear the lines\n needsConnectionDraw = true\n end\n if needsConnectionRebuild then\n rebuildConnectionList()\n end\n if needsConnectionDraw then\n drawDraggingConnections()\n end\nend\n\n-- Global event handler, delegated from Global. Clears any connection lines from dragged cards\n-- before they are destroyed by entering a deck. Removal of the card from the dragging list will\n-- be handled during the next onUpdate() call.\nfunction tryObjectEnterContainer()\n for draggedGuid, _ in pairs(draggingGuids) do\n local draggedObj = getObjectFromGUID(draggedGuid)\n if draggedObj ~= nil then\n draggedObj.setVectorLines({})\n end\n end\nend\n\n---------------------------------------------------------\n-- main functionality\n---------------------------------------------------------\n\nfunction shouldSpawnTokens(card)\n local metadata = JSON.decode(card.getGMNotes())\n if metadata == nil then\n return tokenManager.hasLocationData(card)\n end\n return metadata.type == \"Location\"\n or metadata.type == \"Enemy\"\n or metadata.type == \"Treachery\"\n or metadata.weakness\n -- hardcoded IDs for \"Makeshift Trap\" and \"Shrine of the Moirai\"\n -- these cards are events with uses, that attach to encounter cards and thus will enter play in the playarea\n -- TODO: probably turn this into a metadata field if we get more cards like that\n or metadata.id == \"07310\"\n or metadata.id == \"09100\"\nend\n\n-- Checks the given card and adds it to the list of locations tracked for connection purposes.\n-- A card will be added to the tracking if it is a location in the play area (based on centerpoint).\n---@param card tts__Object A card object, possibly a location.\nfunction maybeTrackLocation(card)\n -- Collision checks for any part of the card overlap, but our other tracking is centerpoint\n -- Ignore any collision where the centerpoint isn't in the area\n if isInPlayArea(card) then\n local metadata = JSON.decode(card.getGMNotes())\n if metadata == nil then\n table.insert(missingData, card)\n else\n if metadata.type == \"Location\" then\n if card.is_face_down then\n locations[card.getGUID()] = metadata.locationBack\n else\n locations[card.getGUID()] = metadata.locationFront\n end\n\n -- only draw connection lines for not-excluded scenarios\n if showLocationLinks() then\n rebuildConnectionList()\n drawBaseConnections()\n end\n end\n end\n end\nend\n\n-- Stop tracking a location for connection drawing. This should be called for both collision exit\n-- and destruction, as a destroyed object does not trigger collision exit. An object can also be\n-- deleted mid-drag, but the ordering for drag events means we can't clear those here and those will\n-- be cleared in the next onUpdate() cycle.\n---@param card tts__Object Card to (maybe) stop tracking\nfunction maybeUntrackLocation(card)\n -- Locked objects no longer collide (hence triggering an exit event) but are still in the play\n -- area. If the object is now locked, don't remove it.\n if locations[card.getGUID()] ~= nil and not card.locked then\n locations[card.getGUID()] = nil\n rebuildConnectionList()\n drawBaseConnections()\n end\nend\n\n-- Builds a list of GUID to GUID connection information based on the currently tracked locations.\n-- This will update the connection information and store it in the locationConnections data member,\n-- but does not draw those connections. This should often be followed by a call to drawBaseConnections()\nfunction rebuildConnectionList()\n if not showLocationLinks() then\n locationConnections = {}\n return\n end\n\n local iconCardList = {}\n\n -- Build a list of cards with each icon as their location ID\n for cardId, metadata in pairs(draggingGuids) do\n buildLocListByIcon(cardId, iconCardList, metadata)\n end\n for cardId, metadata in pairs(locations) do\n buildLocListByIcon(cardId, iconCardList, metadata)\n end\n\n -- Pair up all the icons\n locationConnections = {}\n for cardId, metadata in pairs(draggingGuids) do\n buildConnection(cardId, iconCardList, metadata)\n end\n for cardId, metadata in pairs(locations) do\n if draggingGuids[cardId] == nil then\n buildConnection(cardId, iconCardList, metadata)\n end\n end\nend\n\n-- Extracts the card's icon string into a list of individual location icons\n---@param cardId string GUID of the card to pull the icon data from\n---@param iconCardList table A table of icon-\u003eGUID list. Mutable, will be updated by this method\n---@param locData table A table containing the metadata for the card (for the correct side)\nfunction buildLocListByIcon(cardId, iconCardList, locData)\n if locData ~= nil and locData.icons ~= nil then\n for icon in string.gmatch(locData.icons, \"%a+\") do\n if iconCardList[icon] == nil then\n iconCardList[icon] = {}\n end\n table.insert(iconCardList[icon], cardId)\n end\n end\nend\n\n-- Builds the connections for the given cardID by finding matching icons and adding them to the\n-- Playarea's locationConnections table.\n---@param cardId string GUID of the card to build the connections for\n---@param iconCardList table A table of icon-\u003eGUID List. Used to find matching icons for connections.\n---@param locData table A table containing the metadata for the card (for the correct side)\nfunction buildConnection(cardId, iconCardList, locData)\n if locData ~= nil and locData.connections ~= nil then\n locationConnections[cardId] = {}\n for icon in string.gmatch(locData.connections, \"%a+\") do\n if iconCardList[icon] ~= nil then\n for _, connectedGuid in ipairs(iconCardList[icon]) do\n -- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way\n if locationConnections[connectedGuid] ~= nil\n and (locationConnections[connectedGuid][cardId] == ONE_WAY\n or locationConnections[connectedGuid][cardId] == BIDIRECTIONAL) then\n locationConnections[connectedGuid][cardId] = BIDIRECTIONAL\n locationConnections[cardId][connectedGuid] = nil\n else\n if locationConnections[connectedGuid] == nil then\n locationConnections[connectedGuid] = {}\n end\n locationConnections[cardId][connectedGuid] = ONE_WAY\n locationConnections[connectedGuid][cardId] = INCOMING_ONE_WAY\n end\n end\n end\n end\n end\nend\n\n-- Draws the lines for connections currently in locationConnections but not in draggingGuids.\nfunction drawBaseConnections()\n if not showLocationLinks() then\n locationConnections = {}\n self.setVectorLines({})\n return\n end\n local cardConnectionLines = {}\n\n for originGuid, targetGuids in pairs(locationConnections) do\n -- Objects should reliably exist at this point, but since this can be called during onUpdate the\n -- object checks are conservative just to make sure.\n local origin = getObjectFromGUID(originGuid)\n if draggingGuids[originGuid] == nil and origin ~= nil then\n for targetGuid, direction in pairs(targetGuids) do\n local target = getObjectFromGUID(targetGuid)\n if draggingGuids[targetGuid] == nil and target ~= nil then\n -- Since we process the full list, we're guaranteed to hit any ONE_WAY connections later\n -- so we can ignore INCOMING_ONE_WAY\n if direction == BIDIRECTIONAL then\n addBidirectionalVector(origin, target, self, cardConnectionLines)\n elseif direction == ONE_WAY then\n addOneWayVector(origin, target, self, cardConnectionLines)\n end\n end\n end\n end\n end\n self.setVectorLines(cardConnectionLines)\nend\n\n-- Draws the lines for cards which are currently being dragged.\nfunction drawDraggingConnections()\n if not showLocationLinks() then return end\n local ownedVectors = {}\n\n for originGuid, _ in pairs(draggingGuids) do\n targetGuids = locationConnections[originGuid]\n -- Objects should reliably exist at this point, but since this can be called during onUpdate the\n -- object checks are conservative just to make sure.\n local origin = getObjectFromGUID(originGuid)\n if draggingGuids[originGuid] and origin ~= nil and targetGuids ~= nil then\n ownedVectors[originGuid] = {}\n for targetGuid, direction in pairs(targetGuids) do\n local target = getObjectFromGUID(targetGuid)\n if target ~= nil then\n if direction == BIDIRECTIONAL then\n addBidirectionalVector(origin, target, origin, ownedVectors[originGuid])\n elseif direction == ONE_WAY then\n addOneWayVector(origin, target, origin, ownedVectors[originGuid])\n elseif direction == INCOMING_ONE_WAY and not draggingGuids[targetGuid] then\n addOneWayVector(target, origin, origin, ownedVectors[originGuid])\n end\n end\n end\n end\n end\n for ownerGuid, vectors in pairs(ownedVectors) do\n local card = getObjectFromGUID(ownerGuid)\n card.setVectorLines(vectors)\n end\nend\n\n-- Draws a bidirectional location connection between the two cards, adding the necessary lines to the list\n---@param card1 tts__Object One of the card objects to connect\n---@param card2 tts__Object The other card object to connect\n---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative\n--- positioning and scaling, as well as highlighting connections during a drag operation\n---@param lines table List of vector line elements. Mutable, will be updated to add this connector\nfunction addBidirectionalVector(card1, card2, vectorOwner, lines)\n local cardPos1 = card1.getPosition()\n local cardPos2 = card2.getPosition()\n cardPos1.y = CONNECTION_LINE_Y\n cardPos2.y = CONNECTION_LINE_Y\n\n local pos1 = vectorOwner.positionToLocal(cardPos1)\n local pos2 = vectorOwner.positionToLocal(cardPos2)\n\n table.insert(lines, {\n points = { pos1, pos2 },\n color = vectorOwner == self and connectionColor or DRAGGING_CONNECTION_COLOR,\n thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,\n })\nend\n\n-- Draws a one-way location connection between the two cards, adding the lines to do so to the\n-- given lines list. Arrows will point towards the target card.\n---@param origin tts__Object Origin card in the connection\n---@param target tts__Object Target card object to connect\n---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative\n--- positioning and scaling, as well as highlighting connections during a drag operation\n---@param lines table List of vector line elements. Mutable, will be updated to add this connector\nfunction addOneWayVector(origin, target, vectorOwner, lines)\n -- Start with the BiDi then add the arrow lines to it\n addBidirectionalVector(origin, target, vectorOwner, lines)\n local originPos = origin.getPosition()\n local targetPos = target.getPosition()\n originPos.y = CONNECTION_LINE_Y\n targetPos.y = CONNECTION_LINE_Y\n\n -- Calculate distance to be closer for horizontal positions than vertical, since cards are taller than wide\n local heading = Vector(originPos):sub(targetPos):heading(\"y\")\n local distanceFromCard = DIRECTIONAL_ARROW_DISTANCE * 0.7 + DIRECTIONAL_ARROW_DISTANCE * 0.3 * math.abs(math.sin(math.rad(heading)))\n\n -- Calculate the three possible arrow positions. These are offset by half the arrow length to\n -- make them visually balanced by keeping the arrows centered, not tracking the point\n local midpoint = Vector(originPos):add(targetPos):scale(0.5):moveTowards(targetPos, ARROW_ARM_LENGTH / 2)\n local closeToOrigin = Vector(originPos):moveTowards(targetPos, distanceFromCard + ARROW_ARM_LENGTH / 2)\n local closeToTarget = Vector(targetPos):moveTowards(originPos, distanceFromCard - ARROW_ARM_LENGTH / 2)\n\n if (originPos:distance(closeToOrigin) \u003e originPos:distance(closeToTarget)) then\n addArrowLines(midpoint, originPos, vectorOwner, lines)\n else\n addArrowLines(closeToOrigin, originPos, vectorOwner, lines)\n addArrowLines(closeToTarget, originPos, vectorOwner, lines)\n end\nend\n\n-- Draws an arrowhead at the given position.\n---@param arrowheadPos tts__Vector Centerpoint of the arrowhead to draw (NOT the tip of the arrow)\n---@param originPos tts__Vector Origin point of the connection, used to position the arrow arms\n---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative\n--- positioning and scaling, as well as highlighting connections during a drag operation\n---@param lines table List of vector line elements. Mutable, will be updated to add this arrow\nfunction addArrowLines(arrowheadPos, originPos, vectorOwner, lines)\n local arrowArm1 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver(\"y\", -1 * ARROW_ANGLE):add(arrowheadPos)\n local arrowArm2 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver(\"y\", ARROW_ANGLE):add(arrowheadPos)\n\n local head = vectorOwner.positionToLocal(arrowheadPos)\n local arm1 = vectorOwner.positionToLocal(arrowArm1)\n local arm2 = vectorOwner.positionToLocal(arrowArm2)\n table.insert(lines, {\n points = { arm1, head, arm2 },\n color = vectorOwner == self and connectionColor or DRAGGING_CONNECTION_COLOR,\n thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,\n })\nend\n\n-- count victory points on locations in play area\n---@param highlightOff boolean True if highlighting should be enabled\n---@return. Returns the total amount of VP found in the play area\nfunction countVP(highlightOff)\n local totalVP = 0\n\n for cardId, metadata in pairs(locations) do\n local card = getObjectFromGUID(cardId)\n if metadata ~= nil and card ~= nil then\n if highlightOff == true then\n card.highlightOff(\"Green\")\n end\n\n local cardVP = tonumber(metadata.victory) or 0\n if cardVP ~= 0 and not cardHasClues(card) then\n totalVP = totalVP + cardVP\n if highlightOff == false then\n card.highlightOn(\"Green\")\n end\n end\n end\n end\n\n return totalVP\nend\n\n-- checks if a card has clues on it, returns true if clues are on it\n---@param card tts__Object Card to check for clues\nfunction cardHasClues(card)\n local searchResult = searchLib.onObject(card, \"isClue\")\n return #searchResult \u003e 0\nend\n\n-- highlights all locations in the play area without metadata\n---@param state boolean True if highlighting should be enabled\nfunction highlightMissingData(state)\n for i, obj in pairs(missingData) do\n if obj ~= nil then\n if state then\n obj.highlightOff(\"Red\")\n else\n obj.highlightOn(\"Red\")\n end\n else\n missingData[i] = nil\n end\n end\nend\n\n---------------------------------------------------------\n-- functions for outside calls\n---------------------------------------------------------\n\n-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n-- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n---@param playerColor string Color of the player requesting the shift (used for error messages)\nfunction shiftContentsUp(playerColor)\n shiftContents(playerColor, \"up\")\nend\n\nfunction shiftContentsDown(playerColor)\n shiftContents(playerColor, \"down\")\nend\n\nfunction shiftContentsLeft(playerColor)\n shiftContents(playerColor, \"left\")\nend\n\nfunction shiftContentsRight(playerColor)\n shiftContents(playerColor, \"right\")\nend\n\nfunction shiftContents(playerColor, direction)\n local zone = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaZone\")\n if not zone then\n broadcastToColor(\"Scripting zone couldn't be found.\", playerColor, \"Red\")\n return\n end\n\n for _, object in ipairs(zone.getObjects()) do\n if not (SHIFT_EXCLUSION[object.getGUID()] or object.hasTag(\"displacement_excluded\")) then\n object.translate(SHIFT_OFFSETS[direction])\n end\n end\n Wait.time(drawBaseConnections, 0.1)\nend\n\n-- sets the image of the playarea\n---@param newURL string URL for the new surface image\nfunction updateSurface(newURL)\n local customInfo = self.getCustomObject()\n\n if newURL ~= \"\" and newURL ~= nil and newURL ~= DEFAULT_URL then\n customInfo.image = newURL\n broadcastToAll(\"New Playarea Image Applied\", \"Green\")\n else\n customInfo.image = DEFAULT_URL\n broadcastToAll(\"Default Playarea Image Applied\", \"Green\")\n end\n\n self.setCustomObject(customInfo)\n\n local guid = nil\n if customDataHelper then\n guid = customDataHelper.getGUID()\n end\n\n self.reload()\n\n if guid ~= nil then\n Wait.time(function() updateLocations({ guid }) end, 1)\n end\nend\n\n-- Toggles the tags for the playarea's snap points to limit snapping to locations or not\n-- If matchTypes is false, snap points will be reset to snap all cards\n---@param matchTypes boolean Whether snap points should only snap for the matching card types\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for _, snap in ipairs(snaps) do\n if matchTypes then\n if snap.tags == nil then\n snap.tags = { \"Location\" }\n else\n table.insert(snap.tags, \"Location\")\n end\n else\n snap.tags = nil\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- called by the option panel to enabled / disable location connections\nfunction setConnectionDrawState(state)\n connectionsEnabled = state\n rebuildConnectionList()\n drawBaseConnections()\nend\n\n-- called by the option panel to edit the location connection color\nfunction setConnectionColor(color)\n connectionColor = color\n rebuildConnectionList()\n drawBaseConnections()\nend\n\nfunction onScenarioChanged(scenarioName)\n currentScenario = scenarioName\n if not showLocationLinks() then\n broadcastToAll(\"Automatic location connections not available for this scenario\")\n end\nend\n\nfunction getTrackedLocations()\n return locations\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- Check to see if the given object is within the bounds of the play area (using X and Z coordinates)\n---@param object tts__Object Object to check\n---@return boolean: True if the object is inside the play area\nfunction isInPlayArea(object)\n local bounds = self.getBounds()\n local position = object.getPosition()\n -- Corners are arbitrary since it's all global - c1 goes down both axes, c2 goes up\n local c1 = { x = bounds.center.x - bounds.size.x / 2, z = bounds.center.z - bounds.size.z / 2 }\n local c2 = { x = bounds.center.x + bounds.size.x / 2, z = bounds.center.z + bounds.size.z / 2 }\n\n return position.x \u003e c1.x and position.x \u003c c2.x and position.z \u003e c1.z and position.z \u003c c2.z\nend\n\nfunction showLocationLinks()\n return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario] and connectionsEnabled\nend\n\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- rebuilds local snap points (could be useful in the future again)\nfunction buildSnaps()\n local upperleft = { x = 1.53, z = -1.09 }\n local lowerright = { x = -1.53, z = 1.55 }\n local snaps = {}\n\n -- creates 81 snap points, for uneven rows + columns it makes a rotation snap point\n for i = 1, 9 do\n for j = 1, 9 do\n local snap = {}\n snap.position = {}\n snap.position.x = round(upperleft.x - (upperleft.x - lowerright.x) * (i - 1) / 8, 3)\n snap.position.y = 0.1\n snap.position.z = round(upperleft.z - (upperleft.z - lowerright.z) * (j - 1) / 8, 3)\n\n -- enable rotation snaps for uneven rows / columns\n if (i % 2 ~= 0) and (j % 2 ~= 0) then\n snap.rotation = { 0, 0, 0 }\n snap.rotation_snap = true\n end\n\n table.insert(snaps, snap)\n end\n end\n self.setSnapPoints(snaps)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/PlayArea\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- Location connection directional options\nlocal BIDIRECTIONAL = 0\nlocal ONE_WAY = 1\nlocal INCOMING_ONE_WAY = 2\n\n-- Connector draw parameters\nlocal CONNECTION_THICKNESS = 0.015\nlocal DRAGGING_CONNECTION_THICKNESS = 0.15\nlocal DRAGGING_CONNECTION_COLOR = { 0.8, 0.8, 0.8, 1 }\nlocal DIRECTIONAL_ARROW_DISTANCE = 3.5\nlocal ARROW_ARM_LENGTH = 0.9\nlocal ARROW_ANGLE = 25\n\n-- Height to draw the connector lines, places them just above the table and always below cards\nlocal CONNECTION_LINE_Y = 1.529\n\n-- used for recreating the link to a custom data helper after image change\ncustomDataHelper = nil\n\nlocal DEFAULT_URL = \"http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/\"\n\nlocal SHIFT_OFFSETS = {\n left = { x = 0.00, y = 0, z = 7.67 },\n right = { x = 0.00, y = 0, z = -7.67 },\n up = { x = 6.54, y = 0, z = 0.00 },\n down = { x = -6.54, y = 0, z = 0.00 }\n}\nlocal SHIFT_EXCLUSION = {\n [\"b7b45b\"] = true,\n [\"f182ee\"] = true,\n [\"721ba2\"] = true\n}\nlocal LOC_LINK_EXCLUDE_SCENARIOS = {\n [\"The Witching Hour\"] = true,\n [\"The Heart of Madness\"] = true\n}\n\nlocal locations = {}\nlocal locationConnections = {}\nlocal draggingGuids = {}\nlocal missingData = {}\nlocal collisionEnabled = false\nlocal currentScenario, connectionsEnabled\n\n---------------------------------------------------------\n-- general code\n---------------------------------------------------------\n\nfunction onSave()\n return JSON.encode({\n trackedLocations = locations,\n currentScenario = currentScenario,\n connectionColor = connectionColor,\n connectionsEnabled = connectionsEnabled\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData) or {}\n locations = loadedData.trackedLocations or {}\n currentScenario = loadedData.currentScenario\n connectionColor = loadedData.connectionColor or { 0.4, 0.4, 0.4, 1 }\n connectionsEnabled = loadedData.connectionsEnabled\n end\n\n -- this needs to be here since the playarea will be reloaded when the image changes\n self.interactable = false\n\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n-- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n-- data to the local token manager instance.\n---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\nfunction updateLocations(args)\n customDataHelper = getObjectFromGUID(args[1])\n if customDataHelper ~= nil then\n tokenManager.addLocationData(customDataHelper.getTable(\"LOCATIONS_DATA\"))\n end\nend\n\n---------------------------------------------------------\n-- TTS event handling\n---------------------------------------------------------\n\nfunction onCollisionEnter(collisionInfo)\n if not collisionEnabled then return end\n\n local object = collisionInfo.collision_object\n\n if object.type == \"Deck\" then\n table.insert(missingData, object)\n end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n -- check if we should spawn clues here and do so according to playercount\n if shouldSpawnTokens(object) then\n tokenManager.spawnForCard(object)\n end\n\n -- If this card was being dragged, clear the dragging connections. A multi-drag/drop may send\n -- the dropped card immediately into a deck, so this has to be done here\n if draggingGuids[object.getGUID()] ~= nil then\n object.setVectorLines({})\n draggingGuids[object.getGUID()] = nil\n end\n\n maybeTrackLocation(object)\nend\n\nfunction onCollisionExit(collisionInfo)\n maybeUntrackLocation(collisionInfo.collision_object)\nend\n\n-- Destroyed objects don't trigger onCollisionExit(), so check on destruction to untrack as well\nfunction onObjectDestroy(object)\n maybeUntrackLocation(object)\nend\n\nfunction onObjectPickUp(_, object)\n if object.type ~= \"Card\" then return end\n\n -- onCollisionExit USUALLY fires first, so we have to check the card to see if it's a location we should be tracking\n if showLocationLinks() and isInPlayArea(object) and object.getGMNotes() ~= nil and object.getGMNotes() ~= \"\" then\n local pickedUpGuid = object.getGUID()\n local metadata = JSON.decode(object.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n -- onCollisionExit sometimes comes 1 frame after onObjectPickUp (rather than before it or in\n -- the same frame). This causes a mismatch in the data between dragging the on-table, and\n -- that one frame draws connectors on the card which then show up as shadows for snap points.\n -- Waiting ensures we always do thing in the expected Exit-\u003ePickUp order\n Wait.frames(function()\n if object.is_face_down then\n draggingGuids[pickedUpGuid] = metadata.locationBack\n else\n draggingGuids[pickedUpGuid] = metadata.locationFront\n end\n rebuildConnectionList()\n end, 2)\n end\n end\nend\n\n-- Due to the frequence of onUpdate calls, ensure that we only process any changes once\nfunction onUpdate()\n local needsConnectionRebuild = false\n local needsConnectionDraw = false\n for guid, _ in pairs(draggingGuids) do\n local obj = getObjectFromGUID(guid)\n if obj == nil or not isInPlayArea(obj) then\n draggingGuids[guid] = nil\n needsConnectionRebuild = true\n -- If object still exists then it's outside the area and needs to lose the lines attached to it\n if obj ~= nil then\n obj.setVectorLines({})\n end\n end\n -- Even if the last location left the play area, need one last draw to clear the lines\n needsConnectionDraw = true\n end\n if needsConnectionRebuild then\n rebuildConnectionList()\n end\n if needsConnectionDraw then\n drawDraggingConnections()\n end\nend\n\n-- Global event handler, delegated from Global. Clears any connection lines from dragged cards\n-- before they are destroyed by entering a deck. Removal of the card from the dragging list will\n-- be handled during the next onUpdate() call.\nfunction tryObjectEnterContainer()\n for draggedGuid, _ in pairs(draggingGuids) do\n local draggedObj = getObjectFromGUID(draggedGuid)\n if draggedObj ~= nil then\n draggedObj.setVectorLines({})\n end\n end\nend\n\n---------------------------------------------------------\n-- main functionality\n---------------------------------------------------------\n\nfunction shouldSpawnTokens(card)\n local metadata = JSON.decode(card.getGMNotes())\n if metadata == nil then\n return tokenManager.hasLocationData(card)\n end\n return metadata.type == \"Location\"\n or metadata.type == \"Enemy\"\n or metadata.type == \"Treachery\"\n or metadata.weakness\n -- hardcoded IDs for \"Makeshift Trap\" and \"Shrine of the Moirai\"\n -- these cards are events with uses, that attach to encounter cards and thus will enter play in the playarea\n -- TODO: probably turn this into a metadata field if we get more cards like that\n or metadata.id == \"07310\"\n or metadata.id == \"09100\"\nend\n\n-- Checks the given card and adds it to the list of locations tracked for connection purposes.\n-- A card will be added to the tracking if it is a location in the play area (based on centerpoint).\n---@param card tts__Object A card object, possibly a location.\nfunction maybeTrackLocation(card)\n -- Collision checks for any part of the card overlap, but our other tracking is centerpoint\n -- Ignore any collision where the centerpoint isn't in the area\n if isInPlayArea(card) then\n local metadata = JSON.decode(card.getGMNotes())\n if metadata == nil then\n table.insert(missingData, card)\n else\n if metadata.type == \"Location\" then\n if card.is_face_down then\n locations[card.getGUID()] = metadata.locationBack\n else\n locations[card.getGUID()] = metadata.locationFront\n end\n\n -- only draw connection lines for not-excluded scenarios\n if showLocationLinks() then\n rebuildConnectionList()\n drawBaseConnections()\n end\n end\n end\n end\nend\n\n-- Stop tracking a location for connection drawing. This should be called for both collision exit\n-- and destruction, as a destroyed object does not trigger collision exit. An object can also be\n-- deleted mid-drag, but the ordering for drag events means we can't clear those here and those will\n-- be cleared in the next onUpdate() cycle.\n---@param card tts__Object Card to (maybe) stop tracking\nfunction maybeUntrackLocation(card)\n -- Locked objects no longer collide (hence triggering an exit event) but are still in the play\n -- area. If the object is now locked, don't remove it.\n if locations[card.getGUID()] ~= nil and not card.locked then\n locations[card.getGUID()] = nil\n rebuildConnectionList()\n drawBaseConnections()\n end\nend\n\n-- Builds a list of GUID to GUID connection information based on the currently tracked locations.\n-- This will update the connection information and store it in the locationConnections data member,\n-- but does not draw those connections. This should often be followed by a call to drawBaseConnections()\nfunction rebuildConnectionList()\n if not showLocationLinks() then\n locationConnections = {}\n return\n end\n\n local iconCardList = {}\n\n -- Build a list of cards with each icon as their location ID\n for cardId, metadata in pairs(draggingGuids) do\n buildLocListByIcon(cardId, iconCardList, metadata)\n end\n for cardId, metadata in pairs(locations) do\n buildLocListByIcon(cardId, iconCardList, metadata)\n end\n\n -- Pair up all the icons\n locationConnections = {}\n for cardId, metadata in pairs(draggingGuids) do\n buildConnection(cardId, iconCardList, metadata)\n end\n for cardId, metadata in pairs(locations) do\n if draggingGuids[cardId] == nil then\n buildConnection(cardId, iconCardList, metadata)\n end\n end\nend\n\n-- Extracts the card's icon string into a list of individual location icons\n---@param cardId string GUID of the card to pull the icon data from\n---@param iconCardList table A table of icon-\u003eGUID list. Mutable, will be updated by this method\n---@param locData table A table containing the metadata for the card (for the correct side)\nfunction buildLocListByIcon(cardId, iconCardList, locData)\n if locData ~= nil and locData.icons ~= nil then\n for icon in string.gmatch(locData.icons, \"%a+\") do\n if iconCardList[icon] == nil then\n iconCardList[icon] = {}\n end\n table.insert(iconCardList[icon], cardId)\n end\n end\nend\n\n-- Builds the connections for the given cardID by finding matching icons and adding them to the\n-- Playarea's locationConnections table.\n---@param cardId string GUID of the card to build the connections for\n---@param iconCardList table A table of icon-\u003eGUID List. Used to find matching icons for connections.\n---@param locData table A table containing the metadata for the card (for the correct side)\nfunction buildConnection(cardId, iconCardList, locData)\n if locData ~= nil and locData.connections ~= nil then\n locationConnections[cardId] = {}\n for icon in string.gmatch(locData.connections, \"%a+\") do\n if iconCardList[icon] ~= nil then\n for _, connectedGuid in ipairs(iconCardList[icon]) do\n -- If the reciprocal exists, convert it to BiDi, otherwise add as a one-way\n if locationConnections[connectedGuid] ~= nil\n and (locationConnections[connectedGuid][cardId] == ONE_WAY\n or locationConnections[connectedGuid][cardId] == BIDIRECTIONAL) then\n locationConnections[connectedGuid][cardId] = BIDIRECTIONAL\n locationConnections[cardId][connectedGuid] = nil\n else\n if locationConnections[connectedGuid] == nil then\n locationConnections[connectedGuid] = {}\n end\n locationConnections[cardId][connectedGuid] = ONE_WAY\n locationConnections[connectedGuid][cardId] = INCOMING_ONE_WAY\n end\n end\n end\n end\n end\nend\n\n-- Draws the lines for connections currently in locationConnections but not in draggingGuids.\nfunction drawBaseConnections()\n if not showLocationLinks() then\n locationConnections = {}\n self.setVectorLines({})\n return\n end\n local cardConnectionLines = {}\n\n for originGuid, targetGuids in pairs(locationConnections) do\n -- Objects should reliably exist at this point, but since this can be called during onUpdate the\n -- object checks are conservative just to make sure.\n local origin = getObjectFromGUID(originGuid)\n if draggingGuids[originGuid] == nil and origin ~= nil then\n for targetGuid, direction in pairs(targetGuids) do\n local target = getObjectFromGUID(targetGuid)\n if draggingGuids[targetGuid] == nil and target ~= nil then\n -- Since we process the full list, we're guaranteed to hit any ONE_WAY connections later\n -- so we can ignore INCOMING_ONE_WAY\n if direction == BIDIRECTIONAL then\n addBidirectionalVector(origin, target, self, cardConnectionLines)\n elseif direction == ONE_WAY then\n addOneWayVector(origin, target, self, cardConnectionLines)\n end\n end\n end\n end\n end\n self.setVectorLines(cardConnectionLines)\nend\n\n-- Draws the lines for cards which are currently being dragged.\nfunction drawDraggingConnections()\n if not showLocationLinks() then return end\n local ownedVectors = {}\n\n for originGuid, _ in pairs(draggingGuids) do\n targetGuids = locationConnections[originGuid]\n -- Objects should reliably exist at this point, but since this can be called during onUpdate the\n -- object checks are conservative just to make sure.\n local origin = getObjectFromGUID(originGuid)\n if draggingGuids[originGuid] and origin ~= nil and targetGuids ~= nil then\n ownedVectors[originGuid] = {}\n for targetGuid, direction in pairs(targetGuids) do\n local target = getObjectFromGUID(targetGuid)\n if target ~= nil then\n if direction == BIDIRECTIONAL then\n addBidirectionalVector(origin, target, origin, ownedVectors[originGuid])\n elseif direction == ONE_WAY then\n addOneWayVector(origin, target, origin, ownedVectors[originGuid])\n elseif direction == INCOMING_ONE_WAY and not draggingGuids[targetGuid] then\n addOneWayVector(target, origin, origin, ownedVectors[originGuid])\n end\n end\n end\n end\n end\n for ownerGuid, vectors in pairs(ownedVectors) do\n local card = getObjectFromGUID(ownerGuid)\n card.setVectorLines(vectors)\n end\nend\n\n-- Draws a bidirectional location connection between the two cards, adding the necessary lines to the list\n---@param card1 tts__Object One of the card objects to connect\n---@param card2 tts__Object The other card object to connect\n---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative\n--- positioning and scaling, as well as highlighting connections during a drag operation\n---@param lines table List of vector line elements. Mutable, will be updated to add this connector\nfunction addBidirectionalVector(card1, card2, vectorOwner, lines)\n local cardPos1 = card1.getPosition()\n local cardPos2 = card2.getPosition()\n cardPos1.y = CONNECTION_LINE_Y\n cardPos2.y = CONNECTION_LINE_Y\n\n local pos1 = vectorOwner.positionToLocal(cardPos1)\n local pos2 = vectorOwner.positionToLocal(cardPos2)\n\n table.insert(lines, {\n points = { pos1, pos2 },\n color = vectorOwner == self and connectionColor or DRAGGING_CONNECTION_COLOR,\n thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,\n })\nend\n\n-- Draws a one-way location connection between the two cards, adding the lines to do so to the\n-- given lines list. Arrows will point towards the target card.\n---@param origin tts__Object Origin card in the connection\n---@param target tts__Object Target card object to connect\n---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative\n--- positioning and scaling, as well as highlighting connections during a drag operation\n---@param lines table List of vector line elements. Mutable, will be updated to add this connector\nfunction addOneWayVector(origin, target, vectorOwner, lines)\n -- Start with the BiDi then add the arrow lines to it\n addBidirectionalVector(origin, target, vectorOwner, lines)\n local originPos = origin.getPosition()\n local targetPos = target.getPosition()\n originPos.y = CONNECTION_LINE_Y\n targetPos.y = CONNECTION_LINE_Y\n\n -- Calculate distance to be closer for horizontal positions than vertical, since cards are taller than wide\n local heading = Vector(originPos):sub(targetPos):heading(\"y\")\n local distanceFromCard = DIRECTIONAL_ARROW_DISTANCE * 0.7 + DIRECTIONAL_ARROW_DISTANCE * 0.3 * math.abs(math.sin(math.rad(heading)))\n\n -- Calculate the three possible arrow positions. These are offset by half the arrow length to\n -- make them visually balanced by keeping the arrows centered, not tracking the point\n local midpoint = Vector(originPos):add(targetPos):scale(0.5):moveTowards(targetPos, ARROW_ARM_LENGTH / 2)\n local closeToOrigin = Vector(originPos):moveTowards(targetPos, distanceFromCard + ARROW_ARM_LENGTH / 2)\n local closeToTarget = Vector(targetPos):moveTowards(originPos, distanceFromCard - ARROW_ARM_LENGTH / 2)\n\n if (originPos:distance(closeToOrigin) \u003e originPos:distance(closeToTarget)) then\n addArrowLines(midpoint, originPos, vectorOwner, lines)\n else\n addArrowLines(closeToOrigin, originPos, vectorOwner, lines)\n addArrowLines(closeToTarget, originPos, vectorOwner, lines)\n end\nend\n\n-- Draws an arrowhead at the given position.\n---@param arrowheadPos tts__Vector Centerpoint of the arrowhead to draw (NOT the tip of the arrow)\n---@param originPos tts__Vector Origin point of the connection, used to position the arrow arms\n---@param vectorOwner tts__Object The object which these lines will be set to. Used for relative\n--- positioning and scaling, as well as highlighting connections during a drag operation\n---@param lines table List of vector line elements. Mutable, will be updated to add this arrow\nfunction addArrowLines(arrowheadPos, originPos, vectorOwner, lines)\n local arrowArm1 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver(\"y\", -1 * ARROW_ANGLE):add(arrowheadPos)\n local arrowArm2 = Vector(arrowheadPos):moveTowards(originPos, ARROW_ARM_LENGTH):sub(arrowheadPos):rotateOver(\"y\", ARROW_ANGLE):add(arrowheadPos)\n\n local head = vectorOwner.positionToLocal(arrowheadPos)\n local arm1 = vectorOwner.positionToLocal(arrowArm1)\n local arm2 = vectorOwner.positionToLocal(arrowArm2)\n table.insert(lines, {\n points = { arm1, head, arm2 },\n color = vectorOwner == self and connectionColor or DRAGGING_CONNECTION_COLOR,\n thickness = vectorOwner == self and CONNECTION_THICKNESS or DRAGGING_CONNECTION_THICKNESS,\n })\nend\n\n-- Count victory points from locations in play area\n---@param highlightOff boolean True if highlighting should be enabled\n---@return. Returns the total amount of VP found in the play area\nfunction countVP(highlightOff)\n local totalVP = 0\n\n for cardId, metadata in pairs(locations) do\n local card = getObjectFromGUID(cardId)\n if metadata ~= nil and card ~= nil then\n if highlightOff == true then\n card.highlightOff(\"Green\")\n end\n\n local cardVP = tonumber(metadata.victory) or 0\n if cardVP ~= 0 and not cardHasClues(card) then\n totalVP = totalVP + cardVP\n if highlightOff == false then\n card.highlightOn(\"Green\")\n end\n end\n end\n end\n\n return totalVP\nend\n\n-- Checks if a card has clues on it\n---@param card tts__Object Card to check for clues\n---@return boolean hasClues True if card has clues on it\nfunction cardHasClues(card)\n local searchResult = searchLib.onObject(card, \"isClue\")\n return #searchResult \u003e 0\nend\n\n-- Highlights all locations in the play area without metadata\n---@param state boolean True if highlighting should be enabled\nfunction highlightMissingData(state)\n for i, obj in pairs(missingData) do\n if obj ~= nil then\n if state then\n obj.highlightOff(\"Red\")\n else\n obj.highlightOn(\"Red\")\n end\n else\n missingData[i] = nil\n end\n end\nend\n\n---------------------------------------------------------\n-- functions for outside calls\n---------------------------------------------------------\n\n-- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n-- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n---@param playerColor string Color of the player requesting the shift (used for error messages)\nfunction shiftContentsUp(playerColor)\n shiftContents(playerColor, \"up\")\nend\n\nfunction shiftContentsDown(playerColor)\n shiftContents(playerColor, \"down\")\nend\n\nfunction shiftContentsLeft(playerColor)\n shiftContents(playerColor, \"left\")\nend\n\nfunction shiftContentsRight(playerColor)\n shiftContents(playerColor, \"right\")\nend\n\nfunction shiftContents(playerColor, direction)\n local zone = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaZone\")\n if not zone then\n broadcastToColor(\"Scripting zone couldn't be found.\", playerColor, \"Red\")\n return\n end\n\n for _, object in ipairs(zone.getObjects()) do\n if not (SHIFT_EXCLUSION[object.getGUID()] or object.hasTag(\"displacement_excluded\")) then\n object.translate(SHIFT_OFFSETS[direction])\n end\n end\n Wait.time(drawBaseConnections, 0.1)\nend\n\n-- Sets the image of the playarea\n---@param newURL string URL for the new surface image\nfunction updateSurface(newURL)\n local customInfo = self.getCustomObject()\n\n if newURL ~= \"\" and newURL ~= nil and newURL ~= DEFAULT_URL then\n customInfo.image = newURL\n broadcastToAll(\"New Playarea Image Applied\", \"Green\")\n else\n customInfo.image = DEFAULT_URL\n broadcastToAll(\"Default Playarea Image Applied\", \"Green\")\n end\n\n self.setCustomObject(customInfo)\n\n local guid = nil\n if customDataHelper then\n guid = customDataHelper.getGUID()\n end\n\n self.script_state = onSave()\n self.reload()\n\n if guid ~= nil then\n Wait.time(function() updateLocations({ guid }) end, 1)\n end\nend\n\n-- Toggles the tags for the playarea's snap points to limit snapping to locations or not\n-- If matchTypes is false, snap points will be reset to snap all cards\n---@param matchTypes boolean Whether snap points should only snap for the matching card types\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for _, snap in ipairs(snaps) do\n if matchTypes then\n if snap.tags == nil then\n snap.tags = { \"Location\" }\n else\n table.insert(snap.tags, \"Location\")\n end\n else\n snap.tags = nil\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- called by the option panel to enabled / disable location connections\nfunction setConnectionDrawState(state)\n connectionsEnabled = state\n rebuildConnectionList()\n drawBaseConnections()\nend\n\n-- called by the option panel to edit the location connection color\nfunction setConnectionColor(color)\n connectionColor = color\n rebuildConnectionList()\n drawBaseConnections()\nend\n\nfunction onScenarioChanged(scenarioName)\n currentScenario = scenarioName\n if not showLocationLinks() then\n broadcastToAll(\"Automatic location connections not available for this scenario\")\n end\nend\n\nfunction getTrackedLocations()\n return locations\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- Check to see if the given object is within the bounds of the play area (using X and Z coordinates)\n---@param object tts__Object Object to check\n---@return boolean: True if the object is inside the play area\nfunction isInPlayArea(object)\n local bounds = self.getBounds()\n local position = object.getPosition()\n -- Corners are arbitrary since it's all global - c1 goes down both axes, c2 goes up\n local c1 = { x = bounds.center.x - bounds.size.x / 2, z = bounds.center.z - bounds.size.z / 2 }\n local c2 = { x = bounds.center.x + bounds.size.x / 2, z = bounds.center.z + bounds.size.z / 2 }\n\n return position.x \u003e c1.x and position.x \u003c c2.x and position.z \u003e c1.z and position.z \u003c c2.z\nend\n\nfunction showLocationLinks()\n return not LOC_LINK_EXCLUDE_SCENARIOS[currentScenario] and connectionsEnabled\nend\n\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- Rebuilds local snap points (could be useful in the future again)\nfunction buildSnaps()\n local upperleft = { x = 1.53, z = -1.09 }\n local lowerright = { x = -1.53, z = 1.55 }\n local snaps = {}\n\n -- creates 81 snap points, for uneven rows + columns it makes a rotation snap point\n for i = 1, 9 do\n for j = 1, 9 do\n local snap = {}\n snap.position = {}\n snap.position.x = round(upperleft.x - (upperleft.x - lowerright.x) * (i - 1) / 8, 3)\n snap.position.y = 0.1\n snap.position.z = round(upperleft.z - (upperleft.z - lowerright.z) * (j - 1) / 8, 3)\n\n -- enable rotation snaps for uneven rows / columns\n if (i % 2 ~= 0) and (j % 2 ~= 0) then\n snap.rotation = { 0, 0, 0 }\n snap.rotation_snap = true\n end\n\n table.insert(snaps, snap)\n end\n end\n self.setSnapPoints(snaps)\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PlayArea\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"connectionColor\":{\"a\":1,\"b\":0.4,\"g\":0.4,\"r\":0.4},\"connectionsEnabled\":true,\"trackedLocations\":[]}", "MeasureMovement": false, "Name": "Custom_Token", @@ -20427,9 +20479,279 @@ "Order": 0 }, "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 + "b": 1, + "g": 1, + "r": 1 + }, + "ContainedObjects": [ + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "CardID": 542400, + "ColorDiffuse": { + "b": 0.71324, + "g": 0.71324, + "r": 0.71324 + }, + "CustomDeck": { + "5424": { + "BackIsHidden": true, + "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", + "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2022727271907674847/3938E37E1C41BA1E6F1DE628CE1D108E54C668EA/", + "NumHeight": 1, + "NumWidth": 1, + "Type": 0, + "UniqueBack": false + } + }, + "Description": "", + "DragSelectable": true, + "GMNotes": "{\n \"id\": \"A2023\",\n \"type\": \"Asset\",\n \"class\": \"Mystic\",\n \"cost\": 2,\n \"level\": 2,\n \"traits\": \"Item. Relic. Weapon. Melee.\",\n \"combatIcons\": 1,\n \"cycle\": \"Standalone\"\n}", + "GUID": "9c32e2", + "Grid": true, + "GridProjection": false, + "Hands": true, + "HideWhenFaceDown": true, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "CardCustom", + "Nickname": "Sword Cane (2)", + "SidewaysCard": false, + "Snap": true, + "Sticky": true, + "Tags": [ + "Asset", + "PlayerCard" + ], + "Tooltip": true, + "Transform": { + "posX": 76.333, + "posY": 31.28, + "posZ": -1.331, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 1, + "scaleY": 1, + "scaleZ": 1 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "CardID": 542300, + "ColorDiffuse": { + "b": 0.71324, + "g": 0.71324, + "r": 0.71324 + }, + "CustomDeck": { + "5423": { + "BackIsHidden": true, + "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", + "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2022727271907675521/7CD01B7199EDE77C9E62CC6D2EAFF53D99AF5BC5/", + "NumHeight": 1, + "NumWidth": 1, + "Type": 0, + "UniqueBack": false + } + }, + "Description": "", + "DragSelectable": true, + "GMNotes": "{\n \"id\": \"B2023\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 1,\n \"level\": 3,\n \"traits\": \"Item. Clothing.\",\n \"agilityIcons\": 1,\n \"cycle\": \"Standalone\"\n}", + "GUID": "5cb973", + "Grid": true, + "GridProjection": false, + "Hands": true, + "HideWhenFaceDown": true, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "CardCustom", + "Nickname": "Fine Clothes (3)", + "SidewaysCard": false, + "Snap": true, + "Sticky": true, + "Tags": [ + "Asset", + "PlayerCard" + ], + "Tooltip": true, + "Transform": { + "posX": 76.335, + "posY": 37.222, + "posZ": 5.179, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 1, + "scaleY": 1, + "scaleZ": 1 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "CardID": 266500, + "ColorDiffuse": { + "b": 0.71324, + "g": 0.71324, + "r": 0.71324 + }, + "CustomDeck": { + "2665": { + "BackIsHidden": true, + "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", + "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2038486156990333641/5C6B3E30DDCB25F7DA24B2B7C43688AA2AE4744E/", + "NumHeight": 1, + "NumWidth": 1, + "Type": 0, + "UniqueBack": false + } + }, + "Description": "Item. Weapon. Melee.", + "DragSelectable": true, + "GMNotes": "{\n \"id\": \"b8060\",\n \"type\": \"Asset\",\n \"class\": \"Guardian|Mystic\",\n \"cost\": 3,\n \"level\": 3,\n \"traits\": \"Item. Weapon. Melee.\",\n \"combatIcons\": 1,\n \"willpowerIcons\": 1,\n \"cycle\": \"Beta\"\n}", + "GUID": "a20aef", + "Grid": true, + "GridProjection": false, + "Hands": true, + "HideWhenFaceDown": true, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "CardCustom", + "Nickname": "Dragon Pole (3)", + "SidewaysCard": false, + "Snap": true, + "Sticky": true, + "Tags": [ + "Asset", + "PlayerCard" + ], + "Tooltip": true, + "Transform": { + "posX": 76.429, + "posY": 25.612, + "posZ": 9.512, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 1, + "scaleY": 1, + "scaleZ": 1 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "CardID": 266400, + "ColorDiffuse": { + "b": 0.71324, + "g": 0.71324, + "r": 0.71324 + }, + "CustomDeck": { + "2664": { + "BackIsHidden": true, + "BackURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940352139/A2D42E7E5C43D045D72CE5CFC907E4F886C8C690/", + "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2038486156990333452/327F5C791C48AF81F5EBCF5ED72211543E4DFB33/", + "NumHeight": 1, + "NumWidth": 1, + "Type": 0, + "UniqueBack": false + } + }, + "Description": "Ally. Detective. Police.", + "DragSelectable": true, + "GMNotes": "{\n \"id\": \"b5151\",\n \"type\": \"Asset\",\n \"class\": \"Guardian\",\n \"cost\": 3,\n \"level\": 3,\n \"traits\": \"Ally. Detective. Police.\",\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"cycle\": \"Beta\"\n}", + "GUID": "94f23b", + "Grid": true, + "GridProjection": false, + "Hands": true, + "HideWhenFaceDown": true, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "", + "LuaScriptState": "", + "MeasureMovement": false, + "Name": "CardCustom", + "Nickname": "Alice Luxley (2)", + "SidewaysCard": false, + "Snap": true, + "Sticky": true, + "Tags": [ + "Asset", + "PlayerCard" + ], + "Tooltip": true, + "Transform": { + "posX": 77.107, + "posY": 25.612, + "posZ": 10.175, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 1, + "scaleY": 1, + "scaleZ": 1 + }, + "Value": 0, + "XmlUI": "" + } + ], + "CustomMesh": { + "CastShadows": true, + "ColliderURL": "", + "Convex": true, + "CustomShader": { + "FresnelStrength": 0, + "SpecularColor": { + "b": 1, + "g": 1, + "r": 1 + }, + "SpecularIntensity": 0, + "SpecularSharpness": 2 + }, + "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/2450601198422160026/E21C455A90B78E48E0D03FCA4AF52A9C33C31534/", + "MaterialIndex": 3, + "MeshURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_MSH.obj", + "NormalURL": "", + "TypeIndex": 6 }, "Description": "Put any cards in here to add them to the indices for the player card panel and deck importer.\n\nSelect the 'update index' entry in the context menu of this bag once you've added all cards.\n\nThis can be used for custom cards too.", "DragSelectable": true, @@ -20442,14 +20764,13 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"arkhamdb/HotfixBag\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- A Hotfix bag contains replacement cards for the All Cards Bag, and should\n-- have the 'AllCardsHotfix' tag on the object. Code for the All Cards Bag will\n-- find these bags during indexing, and use them to replace cards from the\n-- actual bag.\n\n-- Tells the All Cards Bag to recreate its indexes. The All Cards Bag may\n-- ignore this request; see the rebuildIndexForHotfix() method in the All Cards\n-- Bag for details.\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n\nfunction onLoad()\n allCardsBagApi.rebuildIndexForHotfix()\n self.addContextMenuItem(\"Update card index\", function() allCardsBagApi.rebuildIndexForHotfix() end)\nend\nend)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id table String ID of the card to retrieve\n ---@return table table\n -- If the indexes are still being constructed, an empty table is\n -- returned. Otherwise, a single table with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", {id = id})\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned\n -- it will be removed from the list and cannot be selected again until a reload\n -- occurs or the indexes are rebuilt, which will refresh the list to include all\n -- weaknesses.\n ---@return string: ID of the selected weakness.\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n return getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list. Note that this is\n -- an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return getAllCardsBag().call(\"getCardsByName\", {name = name, exact = exact})\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean true for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return getAllCardsBag().call(\"getCardsByClassAndLevel\", {class = class, upgraded = upgraded})\n end\n\n AllCardsBagApi.getCardsByCycle = function(cycle)\n return getAllCardsBag().call(\"getCardsByCycle\", cycle)\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return getAllCardsBag().call(\"getUniqueWeaknesses\")\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"arkhamdb/HotfixBag\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"arkhamdb/HotfixBag\")\nend)\n__bundle_register(\"arkhamdb/HotfixBag\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- A Hotfix bag contains replacement cards for the All Cards Bag, and should\n-- have the 'AllCardsHotfix' tag on the object. Code for the All Cards Bag will\n-- find these bags during indexing, and use them to replace cards from the\n-- actual bag.\n\n-- Tells the All Cards Bag to recreate its indexes. The All Cards Bag may\n-- ignore this request; see the rebuildIndexForHotfix() method in the All Cards\n-- Bag for details.\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n\nfunction onLoad()\n allCardsBagApi.rebuildIndexForHotfix()\n self.addContextMenuItem(\"Update card index\", function() allCardsBagApi.rebuildIndexForHotfix() end)\nend\nend)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- internal function to create a copy of the table to avoid operating on variables owned by different objects\n local function returnCopyOfList(data)\n local copiedList = {}\n for _, id in ipairs(data) do\n table.insert(copiedList, id)\n end\n return copiedList\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id string ID of the card to retrieve\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a single table with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", { id = id })\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned it\n -- will be removed from the list and cannot be selected again until a reload occurs\n -- or the indexes are rebuilt, which will refresh the list to include all weaknesses.\n ---@return string: ID of the selected weakness\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list.\n -- Note that this is an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByName\", { name = name, exact = exact }))\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByClassAndLevel\", { class = class, upgraded = upgraded }))\n end\n\n -- Returns a list of cards from the bag matching a cycle\n ---@param cycle string Cycle to retrieve (\"The Scarlet Keys\" etc.)\n ---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByCycle\", { cycle = cycle, sortByMetadata = sortByMetadata }))\n end\n\n -- Constructs a list of available basic weaknesses by starting with the full pool of basic\n -- weaknesses then removing any which are currently in the play or deck construction areas\n ---@param traits? string Trait(s) to use as filter\n ---@return table: Array of weakness IDs which are valid to choose from\n AllCardsBagApi.buildAvailableWeaknesses = function(traits)\n return returnCopyOfList(getAllCardsBag().call(\"buildAvailableWeaknesses\", traits))\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return returnCopyOfList(getAllCardsBag().call(\"getUniqueWeaknesses\"))\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MaterialIndex": -1, "MeasureMovement": false, "MeshIndex": -1, - "Name": "Bag", + "Name": "Custom_Model_Bag", "Nickname": "Additional Player Cards", - "Number": 0, "Snap": true, "Sticky": true, "Tags": [ @@ -20458,159 +20779,14 @@ "Tooltip": true, "Transform": { "posX": 60, - "posY": 1.204, + "posY": 1.481, "posZ": 48, "rotX": 0, - "rotY": 0, + "rotY": 270, "rotZ": 0, - "scaleX": 1.5, - "scaleY": 1.5, - "scaleZ": 1.5 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": 0, - "posY": 0, - "posZ": 0, - "rotX": 270, - "rotY": 0, - "rotZ": 0, - "scaleX": 2, - "scaleY": 2, - "scaleZ": 2 - } - } - ], - "Autoraise": true, - "ColorDiffuse": { - "a": 0.27451, - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/2038486699957629839/74B187339172F55B05CD212F214F5D31B117FDF0/", - "MaterialIndex": 3, - "MeshURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/tuckbox_h_MSH.obj", - "NormalURL": "", - "TypeIndex": 0 - }, - "Description": "Barkham Horror", - "DragSelectable": true, - "GMNotes": "scenarios/meddling_of_meowlathotep.json", - "GUID": "d02940", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "The Meddling of Meowlathotep", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -41.275, - "posY": 3.446, - "posZ": -98.784, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 2.21, - "scaleY": 0.46, - "scaleZ": 2.42 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomAssetbundle": { - "AssetbundleSecondaryURL": "", - "AssetbundleURL": "http://chry.me/tts/catlamp.unity3d", - "LoopingEffectIndex": 0, - "MaterialIndex": 0, - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "308439", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Assetbundle_Bag", - "Nickname": "Barkham Horror", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -60.646, - "posY": -0.631, - "posZ": -80.613, - "rotX": 0, - "rotY": 315, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 0.45, - "scaleZ": 0.45 + "scaleX": 1, + "scaleY": 0.14, + "scaleZ": 1 }, "Value": 0, "XmlUI": "" @@ -20639,7 +20815,7 @@ "ImageURL": "https://i.imgur.com/SBE8GR5.png", "WidthScale": 0 }, - "Description": "Only tracks tokens that actually hit the playmat.\n\nAll credit goes to TadGH!", + "Description": "", "DragSelectable": true, "GMNotes": "", "GUID": "766620", @@ -20650,7 +20826,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagStatTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- button calls respective function in \"Global\"\nfunction onLoad()\n self.createButton({\n click_function = \"handleStatTrackerClick\",\n tooltip = \"Left-Click: Print stats\\nRight-Click: Reset stats\",\n height = 1250,\n width = 1250,\n color = { 0, 0, 0, 0 }\n })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"chaosbag/ChaosBagStatTracker\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"chaosbag/ChaosBagStatTracker\")\nend)\n__bundle_register(\"chaosbag/ChaosBagStatTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- button calls respective function in \"Global\"\nfunction onLoad()\n self.createButton({\n click_function = \"handleStatTrackerClick\",\n tooltip = \"Left-Click: Print stats\\nRight-Click: Reset stats\",\n height = 1250,\n width = 1250,\n color = { 0, 0, 0, 0 }\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[true,0]", "MeasureMovement": false, "Name": "Custom_Token", @@ -20698,7 +20874,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/TokenSpawnTool\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenManager = require(\"core/token/TokenManager\")\nlocal TOKEN_INDEX = {}\n\nTOKEN_INDEX[1] = \"actionToken\"\nTOKEN_INDEX[3] = \"resourceCounter\"\nTOKEN_INDEX[4] = \"damage\"\nTOKEN_INDEX[5] = \"path\"\nTOKEN_INDEX[6] = \"horror\"\nTOKEN_INDEX[7] = \"doom\"\nTOKEN_INDEX[8] = \"clue\"\nTOKEN_INDEX[9] = \"resource\"\n\nlocal stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n}\n\nlocal actionStateTable = {\n [\"Guardian\"] = 1,\n [\"Seeker\"] = 2,\n [\"Rogue\"] = 3,\n [\"Mystic\"] = 4,\n [\"Survivor\"] = 5,\n [\"Neutral\"] = 6\n}\n\n---@param index number Index of the pressed key\n---@param playerColor string Color of the triggering player\nfunction onScriptingButtonDown(index, playerColor)\n local tokenType = TOKEN_INDEX[index]\n if not tokenType then return end\n\n local rotation = { x = 0, y = Player[playerColor].getPointerRotation(), z = 0 }\n local position = Player[playerColor].getPointerPosition() + Vector(0, 0.2, 0)\n local subType = \"\"\n local callback = nil\n\n -- check for subtype of resource based on card below\n if tokenType == \"resource\" then\n for _, obj in ipairs(searchLib.belowPosition(position, \"isCard\")) do\n if not obj.is_face_down then\n local metadata = JSON.decode(obj.getGMNotes()) or {}\n local uses = metadata.uses or {}\n for _, useInfo in ipairs(uses) do\n if useInfo.token == \"resource\" then\n subType = useInfo.type\n break\n end\n end\n break\n end\n end\n \n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local stateID = stateTable[string.lower(subType)]\n if stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n -- check hovered object for \"resourceCounter\" tokens and increase them instead\n elseif tokenType == \"resourceCounter\" then\n local hoverObj = Player[playerColor].getHoverObject()\n if hoverObj then\n if tokenType == hoverObj.getMemo() then\n hoverObj.call(\"addOrSubtract\")\n return\n end\n end\n -- check hovered object for \"damage\" and \"horror\" tokens and increase them instead\n elseif tokenType == \"damage\" or tokenType == \"horror\" then\n local hoverObj = Player[playerColor].getHoverObject()\n if hoverObj then\n if tokenType == hoverObj.getMemo() then\n local stateInfo = hoverObj.getStates()\n local stateId = hoverObj.getStateId()\n if stateId \u003c= #stateInfo then\n hoverObj.setState(stateId + 1)\n return\n end\n end\n end\n -- check for nearest investigator card and change action token state to its class \n elseif tokenType == \"actionToken\" then\n local matColor = playmatApi.getMatColorByPosition(position)\n local result = playmatApi.searchAroundPlaymat(matColor, \"isCard\")\n local investigatorClass = \"Neutral\"\n for j, card in ipairs(result) do\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Investigator\" then\n investigatorClass = metadata.class\n end\n end\n\n local stateID = actionStateTable[investigatorClass]\n if stateID ~= nil and stateID ~= 6 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n end\n\n tokenManager.spawnToken(position, tokenType, rotation, callback)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"util/TokenSpawnTool\")\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/TokenSpawnTool\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenManager = require(\"core/token/TokenManager\")\nlocal TOKEN_INDEX = {}\n\nTOKEN_INDEX[1] = \"universalActionAbility\"\nTOKEN_INDEX[3] = \"resourceCounter\"\nTOKEN_INDEX[4] = \"damage\"\nTOKEN_INDEX[5] = \"path\"\nTOKEN_INDEX[6] = \"horror\"\nTOKEN_INDEX[7] = \"doom\"\nTOKEN_INDEX[8] = \"clue\"\nTOKEN_INDEX[9] = \"resource\"\n\n---@param index number Index of the pressed key\n---@param playerColor string Color of the triggering player\nfunction onScriptingButtonDown(index, playerColor)\n local tokenType = TOKEN_INDEX[index]\n if not tokenType then return end\n\n local rotation = { x = 0, y = Player[playerColor].getPointerRotation(), z = 0 }\n local position = Player[playerColor].getPointerPosition() + Vector(0, 0.2, 0)\n\n -- check for subtype of resource based on card below\n if tokenType == \"resource\" then\n local card\n local hoverObj = Player[playerColor].getHoverObject()\n if hoverObj and hoverObj.type == \"Card\" then\n card = hoverObj\n elseif hoverObj then\n -- use the first card below the hovered object if it's not a card1\n for _, obj in ipairs(searchLib.belowPosition(position, \"isCard\")) do\n card = obj\n break\n end\n end\n\n -- get the metadata from the card and maybe replenish a use\n if card and not card.is_face_down then\n local metadata = JSON.decode(card.getGMNotes()) or {}\n local uses = metadata.uses or {}\n for _, useInfo in ipairs(uses) do\n if useInfo.token == \"resource\" then\n -- artifically create replenish data to re-use that existing functionality\n uses[1].count = 999\n uses[1].replenish = 1\n local matColor = playermatApi.getMatColorByPosition(position)\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n tokenManager.maybeReplenishCard(card, uses, mat)\n return\n end\n end\n end\n -- check hovered object for \"resourceCounter\" tokens and increase them instead\n elseif tokenType == \"resourceCounter\" then\n local hoverObj = Player[playerColor].getHoverObject()\n if hoverObj then\n if tokenType == hoverObj.getMemo() then\n hoverObj.call(\"addOrSubtract\")\n return\n end\n end\n -- check hovered object for \"damage\" and \"horror\" tokens and increase them instead\n elseif tokenType == \"damage\" or tokenType == \"horror\" then\n local hoverObj = Player[playerColor].getHoverObject()\n if hoverObj then\n if tokenType == hoverObj.getMemo() then\n local stateInfo = hoverObj.getStates()\n local stateId = hoverObj.getStateId()\n if stateId \u003c= #stateInfo then\n hoverObj.setState(stateId + 1)\n return\n end\n end\n end\n -- check for nearest investigator card and change action token state to its class \n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(position)\n local class = playermatApi.returnInvestigatorClass(matColor)\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = class }) end\n end\n\n tokenManager.spawnToken(position, tokenType, rotation, callback)\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"util/TokenSpawnTool\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Checker_white", @@ -20742,26 +20918,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -20800,7 +20956,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -20828,26 +20984,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -20886,7 +21022,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -20914,26 +21050,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -20972,7 +21088,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21000,26 +21116,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21058,7 +21154,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21086,26 +21182,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21144,7 +21220,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21172,26 +21248,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21230,7 +21286,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21258,26 +21314,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21316,7 +21352,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21344,26 +21380,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21402,7 +21418,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21430,26 +21446,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21488,7 +21484,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21516,26 +21512,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": 0, - "posY": 0, - "posZ": 0, - "rotX": 270, - "rotY": 0, - "rotZ": 0, - "scaleX": 2, - "scaleY": 2, - "scaleZ": 2 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21574,7 +21550,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21674,26 +21650,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21732,7 +21688,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21760,26 +21716,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21818,7 +21754,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21846,26 +21782,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21904,7 +21820,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -21932,26 +21848,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": 0, - "posY": 0, - "posZ": 0, - "rotX": 270, - "rotY": 0, - "rotZ": 0, - "scaleX": 2, - "scaleY": 2, - "scaleZ": 2 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -21990,7 +21886,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -22018,26 +21914,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -22076,7 +21952,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -22104,26 +21980,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -22162,7 +22018,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -22190,26 +22046,6 @@ "y": 0, "z": 0 }, - "AttachedDecals": [ - { - "CustomDecal": { - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/959719855119695911/931B9829687A20F4DEADB36DA57B7E6D76792231/", - "Name": "dunwich_back", - "Size": 7.4 - }, - "Transform": { - "posX": -0.0021877822, - "posY": -0.08963572, - "posZ": -0.00288731651, - "rotX": 270, - "rotY": 359.869568, - "rotZ": 0, - "scaleX": 2.00000215, - "scaleY": 2.00000238, - "scaleZ": 2.00000262 - } - } - ], "Autoraise": true, "ColorDiffuse": { "a": 0.27451, @@ -22248,7 +22084,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -22374,7 +22210,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22432,7 +22268,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22490,7 +22326,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22548,7 +22384,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22606,7 +22442,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22664,7 +22500,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22722,7 +22558,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22780,7 +22616,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22838,7 +22674,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22896,7 +22732,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -22954,7 +22790,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23012,7 +22848,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23070,7 +22906,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23128,7 +22964,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23186,7 +23022,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23244,7 +23080,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23302,7 +23138,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23360,7 +23196,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23418,7 +23254,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23476,7 +23312,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23534,7 +23370,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23592,7 +23428,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23736,7 +23572,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23794,7 +23630,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23852,7 +23688,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23910,7 +23746,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -23968,7 +23804,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24026,7 +23862,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24084,7 +23920,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24142,7 +23978,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24200,7 +24036,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24258,7 +24094,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24316,7 +24152,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24374,7 +24210,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24432,7 +24268,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24490,7 +24326,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24548,7 +24384,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24606,7 +24442,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24664,7 +24500,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24722,7 +24558,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24780,7 +24616,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24838,7 +24674,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24896,7 +24732,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -24954,7 +24790,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview()\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/Tarotcard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- context menu to manually fix rotation\nfunction onLoad()\n self.addContextMenuItem(\"Rotate Preview\", rotatePreview)\n self.addContextMenuItem(\"Rotate Card+Preview\", rotateSelfAndPreview)\nend\n\n-- rotates the alt_view_angle\nfunction rotatePreview(playerColor)\n Player[playerColor].clearSelectedObjects()\n local angle = self.alt_view_angle\n if angle.y == 0 then\n angle.y = 180\n else\n angle.y = 0\n end\n self.alt_view_angle = angle\nend\n\n-- rotates this card and the preview\nfunction rotateSelfAndPreview(playerColor)\n self.setRotationSmooth(self.getRotation() + Vector(0, 180, 0))\n rotatePreview(playerColor)\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/Tarotcard\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -25036,19181 +24872,12 @@ "z": 0 }, "Autoraise": true, - "Bag": { - "Order": 0 - }, "ColorDiffuse": { + "a": 0.8, "b": 1, "g": 1, "r": 1 }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826278/41B14E1673F5F709A93FDAF0F142B53E18AEA3C9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127641434/3B605FA81140800D2929A2F4FC605C61113815ED/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "395836", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Black Hanekawa", - "Snap": false, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826516/584635E992B674121F4E8C62F1CA259D8D33A9C2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127641670/CAB545E928EDC617CA1314223774D88A2CFA2E19/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "53a482", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tsubusa Hanekawa", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -10.236248, - "posY": 0.960002661, - "posZ": -2.48673582, - "rotX": -0.0000241597045, - "rotY": 180, - "rotZ": 0.0000129868076, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 23.243, - "posY": 1.971, - "posZ": -4.644, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826334/69C1571BD3ED38E6837B38FC2A9784C90D6686EA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127641507/8115C513A8A814EB45C429400A4E9679A4AA27C7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2eea14", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Hitagi Senjougahara", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127589908/773A07E5D19FAFDA6AF71CEBC069175C769EF9ED/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588327/419EF2D650774CCB0500C73CFC43AC77D234747F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "148edc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Bob Jenkins", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -12.1176805, - "posY": 1.12415206, - "posZ": -5.36991358, - "rotX": 351.7117, - "rotY": 270.756348, - "rotZ": 349.5606, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 21.879, - "posY": 1.971, - "posZ": -4.199, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826572/5AB6F8CD81E839C6CFD9555918A3E264300CBF04/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127642047/17C45A9A42806A38F1B15D4358DDA48258D819C4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ea25c3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Yotsugi Ononoki", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127590872/69AEF24314609C572C8EC2443582BE04F2186806/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588477/F570ACBA35CB2F916284EFF8F671646235DA822C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29e06e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lily Chen", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -12.1023951, - "posY": 1.01172352, - "posZ": -4.403691, - "rotX": 357.53363, - "rotY": 270.0128, - "rotZ": 356.5814, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 20.017, - "posY": 1.971, - "posZ": -4.015, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826462/33429FB27677A30F054B0ACF868F9CB80879046C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127641618/DCD566DE13398919BD5F836857CCE5E1B15A4D7A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d9a9ba", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Suruga Kanbaru", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127593415/FA319D0449914FDED7A263FBECEF3FA4714C1905/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588525/D4FA9699B6ED018ED996622689AD6384CA7833D2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b183e2", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Monterey Jack", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -12.0660648, - "posY": 0.960000038, - "posZ": -3.44189286, - "rotX": 0.000154525638, - "rotY": 270.00824, - "rotZ": 0.000190097417, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.494, - "posY": 1.971, - "posZ": -4.498, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826516/584635E992B674121F4E8C62F1CA259D8D33A9C2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127641670/CAB545E928EDC617CA1314223774D88A2CFA2E19/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "3fc084", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tsubusa Hanekawa", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826278/41B14E1673F5F709A93FDAF0F142B53E18AEA3C9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127641434/3B605FA81140800D2929A2F4FC605C61113815ED/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "395836", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Black Hanekawa", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -10.2106543, - "posY": 0.9600028, - "posZ": -1.2446934, - "rotX": -0.00000362714854, - "rotY": 180.000015, - "rotZ": -0.00008925218, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 24.617, - "posY": 1.971, - "posZ": -1.601, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127826395/B3FDC858139FA6C9554725D93DDB9DCFB9D5B29D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127641576/396D35B8A84FBE0C49EEFAE98BD94E15A277FB88/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "38912e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Shinobu Oshino", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 23.641, - "posY": 1.971, - "posZ": -0.706, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127783438/7915A66641DDEEC59738335A0569F7BAECFA5709/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127783370/C8476BFEDBE29BCB732F8B405EB4C3EE690F6407/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "37e0fc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Black Hanekawa", - "Snap": false, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.4572487, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127785929/29B7336804A600EE42390063183E9F387A8CC5E6/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127785872/67138C40F1DE5CE32201F2C85003B8FE670F1604/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "27d1be", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Tsubusa Hanekawa", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.09006, - "posY": 1.05605137, - "posZ": 0.427373916, - "rotX": -0.0000382384969, - "rotY": 0.0361007527, - "rotZ": -0.0000650055663, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 20.774, - "posY": 2.067, - "posZ": -1.265, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127784126/6CF27249FEF4F54E8DAD96DA6C3A3BBD24756AF0/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127784053/AA580BC3F93E7E83936844AFCBAC40CC0EE563C1/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d799b1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Hitagi Senjougahara", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 17.915, - "posY": 2.528, - "posZ": -0.451, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127785226/0CBE7413D09EF71C304561E69D488973B2675AE4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127785008/9650915EDD064263792CFBD6711E037764ECA60E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2808c1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Yotsugi Ononoki", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 26.122, - "posY": 2.067, - "posZ": 1.909, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127784614/C1212DA66187F0A55704B0296256EB1F870C74F6/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127784553/636A86A9E948668712A9061BE31FA9E43BB1098B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ca919a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Suruga Kanbaru", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 25.097, - "posY": 2.067, - "posZ": 2.043, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127785929/29B7336804A600EE42390063183E9F387A8CC5E6/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127785872/67138C40F1DE5CE32201F2C85003B8FE670F1604/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c9242b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Tsubusa Hanekawa", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.4572487, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127783438/7915A66641DDEEC59738335A0569F7BAECFA5709/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127783370/C8476BFEDBE29BCB732F8B405EB4C3EE690F6407/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "37e0fc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Black Hanekawa", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -10.2388783, - "posY": 1.05605257, - "posZ": 0.388642371, - "rotX": 0.00000524104371, - "rotY": 0.03614551, - "rotZ": -0.00002475761, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 23.563, - "posY": 2.067, - "posZ": 1.685, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127786535/1374F15B948BFAFDE4DAD8140DC8DDD8BD509C87/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127786470/DF776DA59AEB125A32EF513421DB73B525CBAFAB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4a725c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Shinobu Oshino", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.552, - "posY": 2.067, - "posZ": 0.949, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": false, - "GMNotes": "", - "GUID": "ed079d", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Monogatari", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.713, - "posY": 2.86, - "posZ": -4.792, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": false, - "GMNotes": "", - "GUID": "7f8782", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "For Other Fan Content", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.32, - "posY": 1.989, - "posZ": 0.156, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "Designed by William \"Lemmingrad\" Jung\n\nTo properly illuminate stands.\n[b]Options[/b]: [b]Lighting[/b]\n1) Set [b]Ambient[/b] to [b]Gradient[/b]. \n2) Turn down [b]Reflection Intensity[/b] to your preference. \n3) Adjust [b]Ambient Intensity[/b] to your preference.\n", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4564ef", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "[b]Arkham Fantasy - Pixel Art Mini-Cards[/b]", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 17.148, - "posY": 3.651, - "posZ": 2.315, - "rotX": 0, - "rotY": 0, - "rotZ": 2, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "Updated - 2022-09-13\n- Added Scarlet Keys Investigator\n- Added Summoned Servitor\n- Added Lily Chen Quad-wielding Butterfly Swords and wearing Track Shoes under [b]Alternatives[/b].\n", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8dc454", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "[b]Arkham Fantasy - Pixel Art Mini-Cards[/b]", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 21.321, - "posY": 2.041, - "posZ": 14.049, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.05692, - "g": 0.32578, - "r": 0 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632950023/32B84D3806148A6BB12304CDBDC1B4B0AEF3BE65/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632961194/758470726C5430CA0E5D55E3B8B96162DD54F145/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "862983", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Summoned Servitor", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632950061/3DD39FE00BCF2D0F995862FF7BEE8F9B07552894/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632961243/4484267F67622813B829B1D24E9C35A200828313/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "94767f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Summoned Servitor", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.30093718, - "posY": 0.960000753, - "posZ": -3.76547241, - "rotX": -0.0000464144723, - "rotY": 180.000031, - "rotZ": -0.00001710464, - "scaleX": 0.801324666, - "scaleY": 1, - "scaleZ": 0.801324666 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 16.201, - "posY": 1.971, - "posZ": -7.486, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.8, - "scaleY": 1, - "scaleZ": 0.8 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632893474/4AF139BBCD80D1C20CB0ACCCB187A59716C129E2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632874602/AD80AD0552BBD9885B868EEE71189AB51029B858/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b3859e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lily Chen (Quadwield)", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632893519/00AEC6496FFB456329C276AF64D8B8272370B461/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632874696/26560CEFEE751DBDD87EBE57FA825307CEE66A40/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "77f37b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lily Chen (Quadwield)", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.09603658, - "posY": 0.960000157, - "posZ": 0.08149373, - "rotX": -0.00002042557, - "rotY": 180.000015, - "rotZ": 0.0000134459406, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.423, - "posY": 1.971, - "posZ": 4.171, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351136953/2E52D40BC0FFD36491AC73AE2DE98B8C0187C977/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351135554/A6535EC60FCCA481D469A5DC1362D9E6CBAAED92/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f5c62d", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Sister \"of Battle\" Mary", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351137004/0BDCE801CF3CFFE731ECBF3C6CA42D36DD6E6A6F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351135608/EF64E1FB541E98B2110568E80521F909FCC65B6A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "329445", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Sister \"of Battle\" Mary", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.76786017, - "posY": 0.9600004, - "posZ": -0.2475957, - "rotX": -0.0000199428159, - "rotY": 180, - "rotZ": 0.00000904094, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.338, - "posY": 3.013, - "posZ": -4.417, - "rotX": 358, - "rotY": 180, - "rotZ": 359, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351154638/AD7164AEFA42443372544B9AE244200A7A6C0701/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351153507/40687A48B0B38B8FC6B2EBF1B1A584F664FF7B82/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a0c8aa", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Ursula Downs", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351154679/8FCE2FFA8B295B437C5862861B8BBB71B805405D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351153563/23860331AAF2DAA0CD5587DB287DDAEEAD6CC70F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ef5e1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Ursula Downs", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 7.01594162, - "posY": 0.959999561, - "posZ": 1.3719486, - "rotX": -0.00007631986, - "rotY": 179.999985, - "rotZ": 0.0000159252013, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.847, - "posY": 3.007, - "posZ": -4.413, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351193465/CE9301CD15AFA8A7B5AF34C950714AB39213DDE6/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351191818/BFC802E32AE2A38228739510216AE1D4A4E4BB69/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8902b4", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Duke", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 29.591, - "posY": 1.971, - "posZ": 19.198, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351180402/300B84479A7E7FBD938886E6FFF983BE099DF983/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351178860/0726567002C8338E97BB3E3746675E696FDC8721/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "be4f57", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "\"Ashcan\" Pete", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 6.076, - "posY": 3.038, - "posZ": -4.601, - "rotX": 0, - "rotY": 180, - "rotZ": 358, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f8c290", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Alternatives", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 13.276, - "posY": 3.288, - "posZ": -3.797, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708456674/D0BD072E388A816E723E14099DB0C0F0F29D1B4A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708453946/632737736F711F40CB028160C41BEA9ABB0E5452/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "41af3c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Gloria Goldberg", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708456715/2E1B1D504A7EA17F427E5277185836A03C6F4979/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708454002/B6EE6E9D045F421A99CD5935D3B0DB8359889E89/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bdbebb", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Gloria Goldberg", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.72389579, - "posY": 0.960001349, - "posZ": -2.41127372, - "rotX": -0.0000258430046, - "rotY": 180, - "rotZ": 0.0000110427845, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.776, - "posY": 3.029, - "posZ": -1.858, - "rotX": 0, - "rotY": 180, - "rotZ": 359, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708457497/56FD4FA183C86788EF3F0D7109C75D61A711BD29/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708454043/58DCCF75F91563D964F0DE40E0466E7C1E397304/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "1d37ae", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Norman Withers", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708457543/F21AA4447E954D0BF945B0C65AEAC3147B94956C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708454082/88D4671EDA79C9A7580390D1CEABD439B3DFE7A8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2db65b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Norman Withers", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.29944789, - "posY": 0.9600009, - "posZ": -2.7570343, - "rotX": -0.0000174139932, - "rotY": 180.000015, - "rotZ": -0.00000132418131, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.753, - "posY": 3.026, - "posZ": -2.204, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7ef8b0", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Novellas Only", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 14.227, - "posY": 3.279, - "posZ": -2.708, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708406881/374951AF62E93DB10E32237BE4E98F7C2C94314D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708410170/D4127E3D1255D89CD6829131FC60722471D2434D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "df5fd1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Duke", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708406916/3FAEBBD786FAB3E42E02D9A7DF4C73F89A5FE9FE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708410213/7D3454C8CD3E81CB7AFADBEE3555DA6CF80A5D16/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5bd561", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Duke", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.37280273, - "posY": 1.06000113, - "posZ": -2.25585771, - "rotX": -0.000048685044, - "rotY": 180, - "rotZ": 180.000031, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.192, - "posY": 3.027, - "posZ": -0.305, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708406425/5A57CC74B357FC9826F5E433109A5D7506AF5B0D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708409781/C504CD93965EC04EBCAD7440CEE2F5CC0CEF9B8A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7aeca6", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jacqueline Canine", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708406473/27D805A6B11B91691FDA338CA981D264C92D98B4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708409829/5B9442CA3BD4B0F37F58505B84EEA6E1AD76A4D9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "9b1574", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jacqueline Canine", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.09026277, - "posY": 0.9600017, - "posZ": -2.29498529, - "rotX": -0.000061689534, - "rotY": 180, - "rotZ": -0.0000379493031, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.655, - "posY": 3.027, - "posZ": -0.344, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708405998/57C5A19E3FDDE6159F7D4C319C40F83CAA9E2958/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708409369/88D089A83D503494EAC46DE1D41B6852D37C7518/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "35a602", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "\"Skids\" O'Droole", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708406061/324040CFDCAD02FA8C33585794D71751C3CCF6FB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708409413/2C5B4CA5C5308BCA94B02FDA37446D2476434744/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "81d8c5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "\"Skids\" O'Droole", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.36366594, - "posY": 0.960001945, - "posZ": -2.128488, - "rotX": -0.00007556884, - "rotY": 180, - "rotZ": -0.0000503773372, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.073, - "posY": 3.036, - "posZ": -0.385, - "rotX": 1, - "rotY": 180, - "rotZ": 1, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708405508/E5578DBC5D96CA60DF4FCD962958B3AB48FBB7FF/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708408923/70406CED274C9EE96A5275083C1472EEB7F42655/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ccff5a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Kate Winthpup", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708405554/F313FAF23B1125C6E1F064177146B0C1726D732B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708408969/3B7B47E8D072406708C65F819662ECC5F4B43CA7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7ee00e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Kate Winthpup", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.76785517, - "posY": 0.9600023, - "posZ": -2.31039453, - "rotX": -0.0000190496849, - "rotY": 180.000015, - "rotZ": -0.00000625381972, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -9.453, - "posY": 3.027, - "posZ": -0.406, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708404997/A7821E94249324720D9EEC18F51C6E0F32511F20/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708408532/033B521E7599C6A651B9BB92D13880FAF1974002/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "41af3c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Bark Harrigan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708405075/B15AEA5DC9BC8532704ADE17AB22E90A2FAA021E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708408571/623901B6B223E8DAD4C28A007D34A10B54DD873C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5887bc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Bark Harrigan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.230135, - "posY": 0.960002542, - "posZ": -2.34120679, - "rotX": -0.0000202591837, - "rotY": 180, - "rotZ": 0.00000968632048, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.923, - "posY": 3.011, - "posZ": 0.641, - "rotX": 4, - "rotY": 180, - "rotZ": 358, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b08c1f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Barkham Horror", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.661, - "posY": 1.786, - "posZ": 0.986, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708355417/A1FCE73454122F3B14CBCFC313660D9D032624F6/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708341095/86B5B824E926576A815D31D26AE3D0A855D78A2E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68c7c8", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Stella Clark", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708355469/E91F08E1796B1E0E4F21E2E1B7EEF68CB35D433B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708341146/6372AE92829917C3270E03943CAB54DC19B2F7B0/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0da48f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Stella Clark", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.90033984, - "posY": 0.960001469, - "posZ": -2.64144874, - "rotX": -0.00005352913, - "rotY": 180.000015, - "rotZ": -0.0000238037519, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.563, - "posY": 3.026, - "posZ": -3.217, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708354798/23612793AE012CDFD906D19FAC167E81F3ED78F7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708340666/402C6AA9EACD3D74A286A7BB3F59FADFED47D95E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8007d3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jacqueline Fine", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708354840/A8B4F12E6DD0A1BC641627FD3170671F34551F10/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708340716/31853BB4E6FB5C7C6EB93B48698DEB3DB9198DFD/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0896ca", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jacqueline Fine", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.402367145, - "posY": 0.9600022, - "posZ": -2.61121845, - "rotX": -0.00001754304, - "rotY": 180, - "rotZ": -0.00000187718229, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.893, - "posY": 3.026, - "posZ": -3.189, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708353668/6FFADF9E7F21046DAEDDCC7C7B049831353C8329/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708340350/3880B5C3B9AEA9B05CA358A0DBAAB87D57E33BF7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "801e8a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Winifred Habbamock", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708353726/A9E04180C3AE495EA6CB2FA331CEBA034B83EB30/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708340389/3602C2AD31DB9A9120811C379C432C1924EB9471/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ee63fb", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Winifred Habbamock", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.52050519, - "posY": 0.9600022, - "posZ": -2.68199062, - "rotX": -0.0000217946163, - "rotY": 180.000015, - "rotZ": -0.000009511086, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.196, - "posY": 3.026, - "posZ": -3.201, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708352413/D2D829214A3D78A3CFE97319A3D3C2B1CAD66AED/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708339924/8530773A311DEA65FB59380D60635DC1510E1D3F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "98607a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Harvey Walters", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708352462/AB9320A7EFEAABB89C81402FE25E2B86D56518D1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708339987/528F0AB144F2CFA1F81E13B0BA38590D0B2D87B3/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f22888", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Harvey Walters", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.77317119, - "posY": 0.960002542, - "posZ": -2.53136683, - "rotX": -0.0000177730763, - "rotY": 180, - "rotZ": -0.00000375366676, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.488, - "posY": 3.03, - "posZ": -3.207, - "rotX": 1, - "rotY": 180, - "rotZ": 358, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708344319/1CBFAE08EF3FDD939AEEC1195F53B6E57F05D098/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708339555/596AB8F4AC36C120431CE65AE962919660CF7F2D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "41af3c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Nathaniel Cho", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708344373/47E1455D596285DA9AC591B8CBBA8A6BBC3A399D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708339610/369551E19D2D282ACDEFE9E828B5693736415C60/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "312943", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Nathaniel Cho", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.185497, - "posY": 1.06000245, - "posZ": -2.944681, - "rotX": -0.00001985872, - "rotY": 180.000015, - "rotZ": 180, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.911, - "posY": 3.026, - "posZ": -3.23, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "151d53", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Starter Decks", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 8.636, - "posY": 1.786, - "posZ": 2.398, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375030660/C6ED3F6B2F5351E103EF1277496BE3088F332458/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375014306/82D626CE83CFE1AE3F3A3B0A521F2DFB177DF83A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ba9781", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Charlie Kane", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375030766/8DDB3114E8EAA4E6644DA807E802C03C6F54CE58/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375034886/D7435766AE971D4EC42ABCB34CFA7A92B1446224/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "77f37b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Charlie Kane", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.76233363, - "posY": 0.9599999, - "posZ": -0.41345, - "rotX": -0.00000302555077, - "rotY": 180.000031, - "rotZ": -0.00000565799564, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.265, - "posY": 2.12, - "posZ": 1.937, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632947355/941B61A3D6771DEA0B0BBE3F603015073D856D40/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632960994/1370ADD64F9B581016C956F7EE4E7BB3A188B2D4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "798bfe", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Darrell Simmons", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632947406/933B7F89F93F09B06C4B089A621D4CE26BF65DB9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632961033/B7EB0A964E3A2D285CD669B65A6D09B80219FB0C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5bd561", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Darrell Simmons", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.1882906, - "posY": 1.06, - "posZ": -0.45706138, - "rotX": -0.00000426442239, - "rotY": 180, - "rotZ": 180, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 16.7, - "posY": 1.971, - "posZ": 14.171, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632946771/4FF34F9BE90B5C5882C6CADF9A172E0E2005BAA8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632960330/90E6E1620ACFD62849623DF312D75DEBA216BC4D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "983a88", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Amina Zidane", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632946814/0D8C9527E570E7F83FC14B94C9FC5C76A263735C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632960426/FCE12D171233548F91BAC47BD2DCA07557FDE6A7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "9b1574", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Amina Zidane", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.7728888, - "posY": 0.9600003, - "posZ": -0.347122, - "rotX": -0.0000249907916, - "rotY": 180.000015, - "rotZ": 0.0000126301475, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 17.686, - "posY": 1.971, - "posZ": 11.669, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632947978/2D4BE572C692C50D61B7F388A68BECBD2E1DACFE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632961069/49380318F33EDA0DC95C9A43322F0AFC8B4E5ECA/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "162b69", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Kymani Jones", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632948332/14ABF11FB8CAFC7377425F01773F10B51B6DCA58/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632961104/698C84C2E7E5A963C804F1BC15DBD0958D3E12FF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "81d8c5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Kymani Jones", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.856147, - "posY": 1.06000054, - "posZ": -0.32208845, - "rotX": -0.0000241434973, - "rotY": 180, - "rotZ": 180, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 18, - "posY": 1.971, - "posZ": 11.686, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632948931/5077C05281A2E04372EBFE2BA974DADB867B8910/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632952090/B031DAA4DB280D0D11A41CF8351A200913C2F558/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "145581", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Vincent Lee", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632948977/750B84BB13732CD535E935D33778FE514E924685/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632952227/81ADF0FEFD4F8AD83F296FC252792378622CFFFF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7ee00e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Vincent Lee", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.512538, - "posY": 0.9600007, - "posZ": -1.78131974, - "rotX": 0.0000602748223, - "rotY": 180.000092, - "rotZ": -0.0000207854464, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.211, - "posY": 1.971, - "posZ": 12.862, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632949464/2D23C92B73A245B0359ACF0FD728CA6D287A491E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632951929/10AD6AC10E9125B22E4F0BF08EB53393DE19CF84/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f44b03", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Carson Sinclair", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632949502/8370097D70B1D2EDEA4FBAA398F86577D05B8FF5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632952034/870E0122E968A8072CC6F5300011507EE611917F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5887bc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Carson Sinclair", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.271518, - "posY": 0.960001945, - "posZ": -0.225683391, - "rotX": -0.00001050811, - "rotY": 180.000076, - "rotZ": -0.00003599694, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 17.477, - "posY": 1.971, - "posZ": 11.928, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4c6a2e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Scarlet Keys", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 15.038, - "posY": 3.288, - "posZ": -2.646, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.96341, - "g": 0.82058, - "r": 0.16281 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127589857/8350D4B8CA0167FD63AE6F4252DB131DCF02068F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127523334/29041B1A9ECCAD66934F7394C1BAAD5A29419D5C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2eea14", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Bob Jenkins", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127589908/773A07E5D19FAFDA6AF71CEBC069175C769EF9ED/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588327/419EF2D650774CCB0500C73CFC43AC77D234747F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5bd561", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Bob Jenkins", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.87873745, - "posY": 0.9599999, - "posZ": 4.86478758, - "rotX": -0.0000698059, - "rotY": 180, - "rotZ": 0.0000213232343, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 13.05, - "posY": 2.025, - "posZ": 0.184, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127590802/89F9AF2C4EAFD5ABDB3E877213F79C0D4A410C78/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127522876/4B0FEBAFA852F2D0A8C5496CF467EEB190EABD6C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ea25c3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lily Chen", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127590872/69AEF24314609C572C8EC2443582BE04F2186806/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588477/F570ACBA35CB2F916284EFF8F671646235DA822C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "9b1574", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lily Chen", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.380549461, - "posY": 1.06000018, - "posZ": 2.61119843, - "rotX": 0.0000382719481, - "rotY": 179.999908, - "rotZ": 180, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 12.838, - "posY": 3.473, - "posZ": 5.945, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127593370/DD3BE0CB11F4308ED5C966D8B6620BB50BD11186/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127522568/63253F03CECE5EDF2042D17AFFA5A1F082DD7ECC/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d9a9ba", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Monterey Jack", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127593415/FA319D0449914FDED7A263FBECEF3FA4714C1905/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588525/D4FA9699B6ED018ED996622689AD6384CA7833D2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "81d8c5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Monterey Jack", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.180552, - "posY": 0.960000157, - "posZ": 4.300206, - "rotX": -0.0000200522372, - "rotY": 179.999969, - "rotZ": -0.00006758973, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 12.61, - "posY": 3.472, - "posZ": 6.315, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127592641/5C7D4DBFC0F62886306691D6596E87C064F353D9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127522098/E3BE272A6CDA2AD18B26FD9A8A2553006DB02EF3/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "3fc084", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Norman Withers", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127592732/BB9E64CC70B4E6BD61EEBDD74839B8E2CBA72E2C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588586/8BC5F352FA3F5C2D4BF09F424A1649604D3974B7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7ee00e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Norman Withers", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.154894, - "posY": 0.960001349, - "posZ": 4.303051, - "rotX": -0.0000342380154, - "rotY": 180.000061, - "rotZ": 0.0000163141249, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 12.821, - "posY": 3.481, - "posZ": 5.84, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127591539/554A5A800A2C267F06CD7E9916E7DDAFEC381EF8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127521616/39573C71B46B811A60950D96773E901A90970259/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "38912e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Daniella Reyes", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127591604/10ED90016BFAA3922CD739B54BD7CBF3BD34C6D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127588406/F938D09732F8FDBDAD79625BDA22820D52B6A5C5/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5887bc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Daniella Reyes", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -9.118571, - "posY": 0.960001349, - "posZ": 4.295325, - "rotX": -0.00007119444, - "rotY": 180, - "rotZ": 0.000005381034, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 11.651, - "posY": 3.434, - "posZ": 7.403, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c4deec", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Edge of the Earth", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 16.654, - "posY": 3.288, - "posZ": 0.415, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.14874, - "g": 0, - "r": 0.70196 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708277471/62E0152FFC85349404B3B952366F83AC3D730133/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1689372753836133538/08468DD39631F30C51A0D315DBE9827C3C2BC1F7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ea374e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Silas Marsh", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708277518/28DA0C3837F55D1D6C6E08FF0B60C09E5ED8E33A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708269049/E0FAA98DADFB179B943E24145E590BD71A1779BF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4a5e7a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Silas Marsh", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.31860137, - "posY": 0.960001469, - "posZ": -2.99149966, - "rotX": -0.0000528490418, - "rotY": 180.000031, - "rotZ": -0.0000302718163, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.658, - "posY": 3.026, - "posZ": -3.208, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708276933/1F855BF79C7F245CD86DF20A6DF3EFD003937DD8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708268582/D82F61CFBC8EC78B52B91DC150B694AEAA8786F8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "cc7ebf", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Dexter Drake", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708276987/583BFF57285786963195B100877F2C46A36E0C63/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708268635/F1EF016E4474C1EC62988C8E36B1BF096DE05A5C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bae011", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Dexter Drake", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.53126442, - "posY": 0.9600016, - "posZ": -2.534034, - "rotX": -0.0000497978253, - "rotY": 180, - "rotZ": -0.0000380652418, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.897, - "posY": 3.026, - "posZ": -3.246, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708275800/C622A40AC5C14098BFB7B0ABE377E6A8A978A76F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708268082/2B7A49FAE3A48E06D00848FC1E13228836AD72D9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "54285c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Trish Scarborough", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708275836/000C6C99DB78208FD56EFE36A6F5E8895BFC67F0/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708268124/F4AFECA42E7336BC62B6D92FBDE56CA5B0C8D099/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "1187f5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Trish Scarborough", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.2628448, - "posY": 0.9600018, - "posZ": -3.1409502, - "rotX": -0.000021589367, - "rotY": 180.000015, - "rotZ": 0.00000440210943, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.233, - "posY": 3.026, - "posZ": -3.237, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708274860/6D669E6A0635375970BFFA53B8E4827C1217CD86/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708267155/357D4F44C80BF3EDA4DE2571580CD51AD0409DAD/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "41af3c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Sister Mary", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708274917/53943BB118CED98CCEFFF4E775A364D6153D8BAD/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708267218/AE7FE2333D8324CE16E72FEA3C75834F077C9E81/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c0b80a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Sister Mary", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.37668073, - "posY": 0.960002065, - "posZ": -2.78234076, - "rotX": -0.000101334554, - "rotY": 180, - "rotZ": -0.00004245368, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.505, - "posY": 1.971, - "posZ": 15.45, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708275352/896FBACDC6BBDCD438BA962B9A02B0D13B597989/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708267572/13622DE74585FC4876A8EFEE8420F8C836BA8519/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e23b96", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Amanda Sharpe", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708275387/ADA399872F35C5633E24ED2963532A2AA50928EA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708267631/1D18D69C99B182A73C6A5FA575DE99B44852E846/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c396fd", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Amanda Sharpe", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.09815109, - "posY": 1.06000173, - "posZ": -2.77054262, - "rotX": -0.00002325564, - "rotY": 180, - "rotZ": 180, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.612, - "posY": 3.031, - "posZ": -3.277, - "rotX": 358, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e65163", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Innsmouth Conspiracy", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 14.211, - "posY": 3.288, - "posZ": -4.093, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.83798, - "g": 0.20585, - "r": 0.39143 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708197974/23DC99948D8444641D9196853ED669B80B4E1458/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708201644/A0164787739161D57DAE1776D9385A761A6DCCA8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "766ad4", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Patrice Hathaway", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708198074/6B49DCCBADD6BC03EA4E6564C5D49ABA6E5DD745/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708201716/30C452E02E8909A9B104CE8ACC0E66DC41976DE2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f1af3a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tony Morgan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.75974548, - "posY": 0.960001945, - "posZ": -2.82789421, - "rotX": -0.000026050433, - "rotY": 180.000015, - "rotZ": 0.00001294456, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.207, - "posY": 3.027, - "posZ": -5.766, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708197291/94ABB4D5AA7291F65EDFB7B2C3CCA6AA3EF0522F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708201100/5858E2373A89F6CB16FEBE701D3946BA9C965997/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "1554fb", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Luke Robinson", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708197345/58932A027E2D20CB188338F45734A0818F3EB38C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708201147/2B427DD014C4FC5209B9AF01F1BC19C79AB0C1F0/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f1af3a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Luke Robinson", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.454549, - "posY": 0.960002065, - "posZ": -2.839298, - "rotX": -0.0000240172212, - "rotY": 180.000015, - "rotZ": 0.000011604704, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.512, - "posY": 3.027, - "posZ": -5.778, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708196300/FB58ECD4C705DDBE1292D2697D813C23C24C9BB9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708200588/B83243F3620B8A3F857EF19BD6C918DB791535E5/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4ac15b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tony Morgan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708196357/33B209F29895F79441FD38C527C7B547347C095D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708200641/15C6BAF9B845A1180C9C02DFD64D8D4466B5B3B9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f1af3a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tony Morgan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.8300723, - "posY": 1.06000137, - "posZ": -2.92916632, - "rotX": -0.0000188975428, - "rotY": 179.999985, - "rotZ": 180.000031, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.796, - "posY": 3.045, - "posZ": -5.867, - "rotX": 1, - "rotY": 180, - "rotZ": 359, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708195342/5351DA7E5E49F475C713410A0E987F1D630C0319/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708199818/DD1907BF3605BF9326C4346C159A2A37E7DAC3BF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7c5bee", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mandy Thompson", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708195418/3027FAE236F7533F31123C4A2151F1C88BF800F7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708199872/294C8959E21BED5D48F93E30897485028A4B7174/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f1af3a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mandy Thompson", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.6790991, - "posY": 0.9600009, - "posZ": -2.9580245, - "rotX": -0.000123189631, - "rotY": 180.000015, - "rotZ": -0.00001606895, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -9.053, - "posY": 3.027, - "posZ": -5.91, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708194665/A34C45D69EC2BE7C20E1805631BB915C013B88F4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708199362/4BD038D582CFD3330C47C61F58EF6A7CE9E1CE1B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "83023e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tommy Muldoon", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708194721/C86B6842A9D4FB9D48FB9D37DAC53258A957403D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708199413/555A8115D647A95F0B148ECF884E01C708EF6016/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2ba63a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tommy Muldoon", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.34762859, - "posY": 1.06000113, - "posZ": -2.98036647, - "rotX": 0.0000202197152, - "rotY": 179.999954, - "rotZ": 180.000214, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -12.146, - "posY": 3.034, - "posZ": 1.967, - "rotX": 359, - "rotY": 180, - "rotZ": 358, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0c3907", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Dream-Eaters", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 9.417, - "posY": 1.787, - "posZ": 2.652, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.20035, - "g": 0, - "r": 0.10799 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708064666/3B3FCB362F89D258EA5CE949974064DEB781AAC7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708059931/C11E59927991E198A6551EAE10D576C21427CFE0/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b47207", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Marie Lambeau", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708064716/CF414F2E1AEA52AC319C8EFA22763DDCA6443BCA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708059970/29895A4A8B99A7FF1A2BA4AD494F291C4694998B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "662b2f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Marie Lambeau", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.57860065, - "posY": 0.960001945, - "posZ": -3.67193651, - "rotX": -0.0000229221332, - "rotY": 180.000015, - "rotZ": 0.000008347389, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.903, - "posY": 3.027, - "posZ": -4.263, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708064098/9E1DB28CC00E4CD43B52E8814A990EC6C1439028/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708059477/A3B4AAD4C8173EF3EE8F99DC1F943E4A55D231EB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "eb0c23", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rita Young", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708064139/800CD01A0EE5917CA4D6AA0AEEBB960ECDFD3768/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708059524/2F6A680EF130769312A480970BB67A7AF0AE694A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b96925", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rita Young", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.22831357, - "posY": 0.9600022, - "posZ": -3.73485827, - "rotX": -0.000023794717, - "rotY": 180, - "rotZ": -1.85044613e-7, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.033, - "posY": 3.027, - "posZ": -4.301, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708063555/2F9E266C40AE0397082EFDD464CEFB434AAA239F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708059083/FE0F13EC3BC4129B83902B4ADDD765A5E197B365/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ca7ab1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Diana Stanley", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708063597/B87F1B817B0D73543775B5CB291408FA791716E8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708059120/46F8A254A2FF410E3089A56C4A26649D1A4333CA/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "dc1db5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Diana Stanley", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.123398937, - "posY": 0.960001349, - "posZ": -3.721062, - "rotX": -0.0000201119165, - "rotY": 180, - "rotZ": -0.0000117004083, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.172, - "posY": 3.027, - "posZ": -4.282, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708062694/41602A79A70F570C0B2BDD3F48EB3E0C7277547D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708058707/B20EB03B233EF19F3DCE2DF867F9F8FCEF02D9E2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d37624", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Preston Fairmount", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708062739/B79E2EFBDC5344261BD995937C005CE0DDE6AFC7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708058754/667A445AD0EAF7962759D5BD349F7B6C53662D28/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "27b98a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Preston Fairmount", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.1744051, - "posY": 0.9600011, - "posZ": -3.82854247, - "rotX": -0.000163961755, - "rotY": 180, - "rotZ": -0.0000239470028, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.403, - "posY": 3.031, - "posZ": -4.313, - "rotX": 359, - "rotY": 180, - "rotZ": 358, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708061628/BD683C1BBF4D6284DBF6456B9BF21CDE56875D38/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708058198/E270E67DFC8D0CCF6B3F27ADF8252017F35860C5/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5f7f8b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Joe Diamond", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708061703/26265EF57B419FD1EE547B539BA791A34010DA6D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708058234/BFDAF1FB116F6CB0DBAC6E5E2EA4C6D169270B07/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4d96a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Joe Diamond", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.83795786, - "posY": 0.9600017, - "posZ": -4.145151, - "rotX": -0.00007161909, - "rotY": 180.000015, - "rotZ": -0.00009196562, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -9.555, - "posY": 3.027, - "posZ": -4.319, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708060895/93E0CEC02A71E77B025DEBE04062D565ABD65D11/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708057800/291702BF8534F8B2243E83E9A26970495545BE76/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a038ff", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Carolyn Fern", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708060953/2D7FA90625496CF0D2AD658E89B1853964C842EC/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708057853/34252766A27380A65E779226890EA55F9C08818E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bba1b0", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Carolyn Fern", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.584668, - "posY": 0.960001945, - "posZ": -3.87626958, - "rotX": -0.0000132751647, - "rotY": 180.000046, - "rotZ": -0.0000345373155, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -9.927, - "posY": 3.036, - "posZ": -4.345, - "rotX": 359, - "rotY": 180, - "rotZ": 358, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708041558/B18C704D029D487F5144ED43C59EFA59663CA809/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044314/922727E14CC8730ADE0AB75353A4B64C60638667/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7aeedd", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Penny White", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708041617/715DCD1F1ACB93954D25583FD9544D29271A015E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044371/943EA7598178161B4718C9DD5D64C65F489CE35F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "69a021", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Penny White", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.586978257, - "posY": 0.9599977, - "posZ": 9.179146, - "rotX": 0.0001862956, - "rotY": 179.998123, - "rotZ": 0.000128975968, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.876, - "posY": 2.483, - "posZ": 6.451, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708040670/D6E2F0956336D577BC309FEF99BA4F6A8BEA10B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044431/AEFDF24400F85B660E1510B6A9546BEF8AE33A82/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8b4fcc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Valentino Rivas", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708040724/4EC16CEEFEC119C5BEB75F6E7444FE4515CA36D4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044475/DAAE1705D3922D41DA166661D2BBC990470CFF61/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "57e394", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Valentino Rivas", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.422660261, - "posY": 0.960000038, - "posZ": 9.16702652, - "rotX": -0.000156121721, - "rotY": 179.99794, - "rotZ": -0.0000416127332, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.804, - "posY": 2.571, - "posZ": 6.467, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708043081/D91383186F9B8AA422B0811DD8F5522EDB5A14D7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044217/CE2E92945A690E13C7FE95586387811FA95048ED/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "eb6c8f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jerome Davids", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708043122/B0840FC4C8EC8E3971B33A0E14A8E43D8C4EEE6A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044267/AB7EE8AA3481039EB6F147FA7626D0076C3188E3/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0d9632", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jerome Davids", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.16228342, - "posY": 0.959998369, - "posZ": 9.223023, - "rotX": -0.00008221584, - "rotY": 179.999969, - "rotZ": -0.00008264612, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.738, - "posY": 2.801, - "posZ": 6.367, - "rotX": 14, - "rotY": 177, - "rotZ": 7, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708042402/D2901AC03697A42E54228B5D8D7F96B34DBD5720/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044131/DC186D07DAB34575B1A3544784FBE993C11E229B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f9cba2", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Gavriella Mizrah", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708042460/684FE39DC4954AAA65F13CE0E7776ED1480DD717/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708044175/0876392599AB0CAF855CA828A4F72E8B01F6B056/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "77f37b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Gavriella Mizrah", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.490250677, - "posY": 0.960000038, - "posZ": 9.250848, - "rotX": -0.000178494083, - "rotY": 179.998016, - "rotZ": -0.0000401837751, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.322, - "posY": 3.029, - "posZ": 7.753, - "rotX": 359, - "rotY": 180, - "rotZ": 359, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bf6b32", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Prologue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 7.598, - "posY": 2.859, - "posZ": 6.621, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f2acef", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Circle Undone", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 8.805, - "posY": 1.786, - "posZ": 2.844, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.15196, - "g": 0, - "r": 0.41289 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.15294, - "g": 0, - "r": 0.41176 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508538/25188B78E433A1487103B4D7FEBF8660AEBDB017/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708494885/788378B90865B37A2AAF16F0E5E78F04E4B02828/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "db5660", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508579/64A0C0B09767C447CC1441E1B54BA67FB66DA3BA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708494922/D8FB1A9F81A9280B7FE5DBD7B4C2AB7F47ACFDCE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ab31aa", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.855447, - "posY": 0.9600004, - "posZ": -3.09236574, - "rotX": 0.0000100537472, - "rotY": 180.000076, - "rotZ": -0.00002682792, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.202, - "posY": 3.021, - "posZ": -5.563, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508538/25188B78E433A1487103B4D7FEBF8660AEBDB017/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708495147/5E40C14ABB1F144C335EDF3C38B474546EF9945F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ae05ab", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508579/64A0C0B09767C447CC1441E1B54BA67FB66DA3BA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708495191/C7765026DA0DC1C0CA01830224F04468A6B5CBBD/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ab31aa", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.1168685, - "posY": 0.9600005, - "posZ": -3.15264, - "rotX": -0.000025679552, - "rotY": 180.0001, - "rotZ": 0.0000133664835, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.94, - "posY": 3.021, - "posZ": -5.623, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508538/25188B78E433A1487103B4D7FEBF8660AEBDB017/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708494812/35FCC25E42DEEFBDEE5532239458267C6A2249FE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7edb33", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508579/64A0C0B09767C447CC1441E1B54BA67FB66DA3BA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708494846/558E4AE3FCAB615FF4DD53792BC437859380AB97/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ab31aa", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.1984168, - "posY": 0.96000123, - "posZ": -3.264808, - "rotX": -0.0000200939721, - "rotY": 180, - "rotZ": -0.0000099067, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.257, - "posY": 3.027, - "posZ": -5.737, - "rotX": 358, - "rotY": 180, - "rotZ": 1, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508538/25188B78E433A1487103B4D7FEBF8660AEBDB017/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708494976/72A53130063D24E953167BE7B0CA1AAC70C79DB6/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "864f65", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508579/64A0C0B09767C447CC1441E1B54BA67FB66DA3BA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708495014/AF340EF63C3FA81AB77B57C304505D3D1E45D42A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ab31aa", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.30559611, - "posY": 0.9600012, - "posZ": -3.40010428, - "rotX": -0.0000209806967, - "rotY": 180.000046, - "rotZ": -0.000008162348, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.752, - "posY": 3.021, - "posZ": -5.871, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508538/25188B78E433A1487103B4D7FEBF8660AEBDB017/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708495053/9F1C7D1CF0A5F67422EB475891FFBC8B4AAE7332/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59d9b9", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508579/64A0C0B09767C447CC1441E1B54BA67FB66DA3BA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708495101/BC22F1A3252365C98CD5E4CF8B90D5B7BF5D5D94/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ab31aa", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.87626529, - "posY": 1.01478851, - "posZ": -3.51791, - "rotX": -0.00464852853, - "rotY": 179.888611, - "rotZ": 6.892589, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.19, - "posY": 3.021, - "posZ": -6.041, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508538/25188B78E433A1487103B4D7FEBF8660AEBDB017/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708494679/FCF06130784FD5EDD0694D5794017F9ACB052315/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "41af3c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708508579/64A0C0B09767C447CC1441E1B54BA67FB66DA3BA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708494734/076A18365A14C7E7F89184FDFFBB50B3B539C586/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ab31aa", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.70303059, - "posY": 0.960002065, - "posZ": -3.69831514, - "rotX": -0.0000212357427, - "rotY": 180.000015, - "rotZ": 0.0000123184545, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.76, - "posY": 3.021, - "posZ": -6.169, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "cdd5b5", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "City of Archives", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 17.622, - "posY": 1.792, - "posZ": -9.515, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707758465/E5CA9A3BF3B3179F1A10B02570EC798997022E8D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707750962/F45A0066CD7BE9E910401289D2F9BB1F313A5299/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c1f75b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Calvin Wright", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707758516/DE0C25D639BA250F1233C2E23C9556A7475ACCB8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707751017/03DB96699627ADEB657478D40C69B39A9ED58B61/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "57c445", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Calvin Wright", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.716913, - "posY": 0.960000038, - "posZ": -2.35708261, - "rotX": -0.0000148394292, - "rotY": 180.000015, - "rotZ": -0.0000252172267, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.619, - "posY": 3.016, - "posZ": -7.014, - "rotX": 358, - "rotY": 180, - "rotZ": 2, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707758059/FF47A21028678562DF0ED3B4DF7D7814FE0281E0/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707750559/FFC924E25985685F14BACCD3B2ADF1CB37D9B1C4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f8d9b3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Father Mateo", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707758100/8FC7921B1688B0C0059DBA6E920FB97740065E72/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707750616/8FE56DC4942B76B85B58969A9C4C31D15BE83DFB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "00daa5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Father Mateo", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.89721358, - "posY": 0.960000753, - "posZ": -3.04599237, - "rotX": 0.0000285328788, - "rotY": 180.000015, - "rotZ": -0.0000414255665, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.34, - "posY": 3.023, - "posZ": -7.006, - "rotX": 359, - "rotY": 180, - "rotZ": 1, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707757545/03D2D6CD390A00A82423C49D687A8CC47DFA43D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707750170/5332C0513469EABC4C2F1B07EDAAE32F39770E70/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "46ca51", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Finn Edwards", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707757588/3537B8A94D9A6F042DA905329477EFD81391474A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707750209/A710646322E7116EC3EBBB1254E9B4A68D6B7CE2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "fc0b63", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Finn Edwards", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.196925431, - "posY": 0.9600012, - "posZ": -2.99892831, - "rotX": -0.0000180291991, - "rotY": 180, - "rotZ": -0.00000261345076, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.618, - "posY": 3.022, - "posZ": -7.048, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707756935/8D43B642B7250E7AFE9AD3B4AE6D49EE01EA3665/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707749650/EDF46057C37CA94C52D4A3AF49BE78028530E121/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "56e051", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Ursula Downs", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707756990/F7769C9D015E6143276B37701234D4AD282B5404/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707749696/773DAB8DB22BE7297D18D94B2940525FBBE17307/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ba45a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Ursula Downs", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.201306313, - "posY": 0.960001, - "posZ": -2.90807033, - "rotX": -0.000077487, - "rotY": 180, - "rotZ": -0.00005331672, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 26.709, - "posY": 1.971, - "posZ": 23.639, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707756335/CC5AFDEA8E26679935805BC32838833E8B12F215/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707749544/BD7A64A90A1FFA471FA4856CB29CCC273B18C7AE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0d621f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Leo Anderson", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707756395/E57E07E9A854840BE33D61949BC2B4E3BAC1BFBA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707749604/41DB7C12949B64EA6051C07CF09126D37A16D951/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "839a34", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Leo Anderson", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.64729381, - "posY": 0.960001469, - "posZ": -2.82980585, - "rotX": -0.0000217961933, - "rotY": 180, - "rotZ": 0.0000116313149, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.374, - "posY": 3.034, - "posZ": -0.189, - "rotX": 359, - "rotY": 180, - "rotZ": 1, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d32231", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Forgotten Age", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 15.585, - "posY": 3.288, - "posZ": 1.935, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.89346, - "r": 0.94251 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707599222/202D33AD1FFA1D282B483A6EDC7CC03D85595E38/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707579964/900F66C54FA2AB64A74E93569BB414CA3C4BC281/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "cbf983", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lola Hayes (Neutral)", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707599222/202D33AD1FFA1D282B483A6EDC7CC03D85595E38/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707580357/8291134B86F73EE0134CA1C7BB672B9B3F04ADC6/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5f42f1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lola Hayes (Guardian)", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.67580843, - "posY": 0.9600007, - "posZ": -3.740093, - "rotX": -0.0000153140772, - "rotY": 180, - "rotZ": -0.0000969686662, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707599222/202D33AD1FFA1D282B483A6EDC7CC03D85595E38/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707580485/4B830419B7F3AEB58666165001877E1A415B47C9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "47736a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lola Hayes (Seeker)", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.67581034, - "posY": 0.960000753, - "posZ": -3.740094, - "rotX": -0.00006143066, - "rotY": 180.000015, - "rotZ": -0.0000319443279, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707599222/202D33AD1FFA1D282B483A6EDC7CC03D85595E38/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707580435/C82EAFF3B0E5C77E27118419AD91257EFF8EF922/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b7003f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lola Hayes (Rogue)", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.67581224, - "posY": 0.9600008, - "posZ": -3.74009514, - "rotX": -0.0000181608866, - "rotY": 180.000031, - "rotZ": -0.00009431026, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707599222/202D33AD1FFA1D282B483A6EDC7CC03D85595E38/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707580397/65C490DDEF18EDEAD3EDD3AFE943C78B506B0044/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "09fa62", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lola Hayes (Mystic)", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.67581367, - "posY": 0.9600006, - "posZ": -3.740096, - "rotX": -0.00001848765, - "rotY": 180.000061, - "rotZ": -0.000100223086, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707599222/202D33AD1FFA1D282B483A6EDC7CC03D85595E38/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707580532/FCF2E57178A974ADCAF999CC50134D280CD16AE0/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ecb749", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Lola Hayes (Survivor)", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.675815, - "posY": 0.9600008, - "posZ": -3.74009681, - "rotX": -0.0000598378974, - "rotY": 180.000046, - "rotZ": -0.0000276226965, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.98, - "posY": 3.027, - "posZ": -5.823, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707593832/B8C4E2D75CEFB6C2CC167855163F2A6904E751FE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707579404/BF94E6FC0D4EC9C4916F23C8A1923A0CD558398A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0c7474", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "William Yorick", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707593898/33285F298EF3C32C8DE080A330129CFFC3B34BFC/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707579454/B5C11E1718148750AA9BD6EBEFA3B61A28940D6A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "dacc50", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "William Yorick", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.68896341, - "posY": 0.9600022, - "posZ": -7.654583, - "rotX": -0.0000245705087, - "rotY": 180, - "rotZ": 0.00001108417, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.396, - "posY": 3.027, - "posZ": -5.886, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707592723/70375E6B3B8398F8882538F8F5681685B0F77222/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707578462/CCF763DA8A203AD1F5BE8ABB07AEAEA4C1B3ABD8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f88bf1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Akachi Onyele", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707592774/0A9B3E42B422F90108EFABC21379DB6E26051F17/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707578505/A04B17BD536BC2DC33F5F070E5D58694A9527032/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "cd565d", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Akachi Onyele", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.07932115, - "posY": 0.960002065, - "posZ": -6.579502, - "rotX": -0.0000175458481, - "rotY": 180.000015, - "rotZ": -0.0000234869785, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.789, - "posY": 3.027, - "posZ": -5.848, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707593340/B0EED020029C8BE786CC86AC0950DE92EB8A0DE1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707579295/FB9F7FD8DC7DC451E702FB89303BF5869C1D6219/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7b1d63", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Sefina Rousseau", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707593391/4DF8A84A64A495829361DB6CE123BD576D52F290/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707579352/48339D32236F9D45D41870ECB9FEDD39649491BF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "117a1f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Sefina Rousseau", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.7471178, - "posY": 0.960002542, - "posZ": -7.25411367, - "rotX": -0.0000140798393, - "rotY": 180.000015, - "rotZ": -0.0000345095068, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.057, - "posY": 3.033, - "posZ": -5.896, - "rotX": 359, - "rotY": 180, - "rotZ": 2, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707583229/302295141EC60FA90CB2CA97A4DB56EDDB7B9CEA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707575701/89C1B8B59FCD52612A43D9817898F3472C190416/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "96d784", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Minh Thi Phan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707583273/7EB0882BCC380F605F3A35394A5468FB16AA1822/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707575755/A90C4EA9E4ACCAAEA025F88F4367700F1605BF10/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "179489", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Minh Thi Phan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.5513342, - "posY": 0.9600035, - "posZ": -6.75175858, - "rotX": -0.0000196382116, - "rotY": 180, - "rotZ": -0.0000302018616, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.327, - "posY": 3.027, - "posZ": -5.969, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707582720/274DE982E6EF7CC6CE291A5501F0529FAF89D8FD/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707575606/D310D938F2F80BE2AA945D11D5A00DE859E0B7D2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0d621f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mark Harrigan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707582776/E9038E182C6A99AFB84404CAD53D74B5EB214B47/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707575664/8F44118C6EB1153BA870842D850CF23029766D92/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ecf2a7", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mark Harrigan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.923844, - "posY": 0.960001945, - "posZ": -3.94195914, - "rotX": -0.0000243047252, - "rotY": 180, - "rotZ": 0.0000112649741, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -9.58, - "posY": 3.027, - "posZ": -6.025, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b491bc", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Path to Carcosa", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 13.893, - "posY": 2.283, - "posZ": 1.396, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.17373, - "g": 0.25261, - "r": 0 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707437839/DF4CBA1822068BBA831D3EB8AD13474FB8DBCA81/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707435937/D3FD68FB9D25C45F4351E231A1A1500F8B52B0E8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0d621f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "\"Ashcan\" Pete", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707437877/A17FC0F81149FD5750D725CEA1CB8E2E2990CAE2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707435976/099E4506B9C55A529BEE7202D3A9E62162B95C14/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b1ef91", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "\"Ashcan\" Pete", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.62513852, - "posY": 0.9600011, - "posZ": -7.34163046, - "rotX": -0.000219753332, - "rotY": 180, - "rotZ": -0.0000321620137, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.813, - "posY": 1.971, - "posZ": -4.712, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707440813/6326948A8D087B2101B9B283C1856EFB54F8E580/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436133/DC2C6B48A6F101ADFEDC5CA8ADDC6F2D7FA1D384/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "42a760", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jim Culver", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707440855/BC820789B1008EC280F57C9D88D56F1754B0E57F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436175/12917AAB8964F6124B3CDF7FFC0FAE69BBFA4C82/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "682611", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jim Culver", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.24929881, - "posY": 0.960000753, - "posZ": -3.83163, - "rotX": -0.0000596265854, - "rotY": 180.000031, - "rotZ": -0.00003143159, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.528, - "posY": 3.027, - "posZ": -7.831, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707439869/3AA87F4C885E1928F938394AF7754160FCC0694D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436022/F40952A2FE9BB173600E2F9CE67BCEDBCFEEABAB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "56ca21", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jenny Barnes", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707439928/BDA49447868E6DC922CA397493CBBABA7C904C23/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436065/6A5BB76DE8B25F6D7C83B3CDA901357B8ED083A3/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "93e70e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Jenny Barnes", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.41232944, - "posY": 0.9600029, - "posZ": -6.38804436, - "rotX": -0.0000206030254, - "rotY": 180, - "rotZ": -0.0000189492821, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.767, - "posY": 3.043, - "posZ": -7.821, - "rotX": 1, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707438760/FAC759B3ADCD73C2FC051AB3615E617C786D5183/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436215/18CA3FE1CE4A7591CE9C57673CCDF38F436B1BD9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5996b0", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rex Murphy", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707438830/73F639A55F38437B85A9B8F5E28431C5BD87989C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436256/F4B3B32406F832907733CCFDC02F9292B9232DE2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e09522", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rex Murphy", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.849555, - "posY": 0.9600011, - "posZ": -3.68585539, - "rotX": -0.000162696844, - "rotY": 180, - "rotZ": -0.0000229980924, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.86, - "posY": 3.027, - "posZ": -7.844, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707434611/45CAB2711B641D5400837665F1D6CD3D16B27C8D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436303/6B67F7F71CACE92D53DB204A6643392000EC5BE2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "60372a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Zoey Samaras", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707434655/CE8F32FF4549A14B8BADE3C0E48F68F0E61B72FC/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707436343/8E7FB54B7D1A9FD6D037A4AE044A257580EC7BB8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "89d111", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Zoey Samaras", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.203708, - "posY": 0.960002065, - "posZ": -3.620548, - "rotX": -0.0000252560785, - "rotY": 180.000015, - "rotZ": 0.000008123292, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -8.991, - "posY": 3.027, - "posZ": -7.855, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "315223", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Dunwich Legacy", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 10.731, - "posY": 1.786, - "posZ": 2.23, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.65331, - "g": 0.08469, - "r": 0 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707202899/CFBE0B6D3BA95995F379A602C5A8A1A583D00793/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707202088/2216F76282F4A4FCB1778C7E5E574B88B001D379/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e6d299", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Wendy Adams", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707202960/E1CD12C981BBAAFACA669288E245EE18EA70323E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707202149/4A0FD0F0B7A2D889E50B39F972595AC00A2C8A8F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29385d", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Wendy Adams", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.96192133, - "posY": 1.060002, - "posZ": -5.86200142, - "rotX": -0.000135441631, - "rotY": 180, - "rotZ": 180.0001, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 22.153, - "posY": 1.971, - "posZ": 6.95, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708116857/F6525F4381108EA389FCB0A907025891D3C0A560/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707198420/4DFF0CA80A65CF166F3455373A5226C153CE7A7A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "01470e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Agnes Baker", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230708116942/214E8832F3F632A6092FB4D9788B42C73EA70A4E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707198474/49327AD91E6F8064ACB52160E7AA58D26241A7A8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "920f62", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Agnes Baker", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.53456926, - "posY": 1.06000042, - "posZ": -2.58590031, - "rotX": -0.000004193393, - "rotY": 180, - "rotZ": 180, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 38.457, - "posY": 1.971, - "posZ": 14.892, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707188729/E4081F8F55E530E269E390A35613A8D4D311BD02/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707186936/7AE575933AB57B52351134E4952A625D6D2297A4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e8f6cd", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "\"Skids\" O'Toole", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707188794/AF0E4CD6B11F907B85D09F26651E3B0EE961FCA3/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707186988/9C5CF0742C6EBC8238C92E195B1D5AAB1B4D7B22/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "150597", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "\"Skids\" O'Toole", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.395289838, - "posY": 1.06000257, - "posZ": -5.907174, - "rotX": -0.000165865713, - "rotY": 179.999985, - "rotZ": 180.000122, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 35.114, - "posY": 1.971, - "posZ": 15.102, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707152276/A9BCA599A2FAA24875E29E091F8EED45D1E9F905/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707154662/243E17ADB4A0E7510E07DD354F805D349E03E287/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5bc05f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Daisy Walker", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707152339/6A5CB834C2F0D64D0932908D87BD789D695DED00/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707154723/4CEE958BF7444019B4C4AC3186006C76C4314545/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bc30ce", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Daisy Walker", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.526291, - "posY": 1.06000268, - "posZ": -5.908639, - "rotX": -0.0000118843254, - "rotY": 180, - "rotZ": 180.000046, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.938, - "posY": 1.971, - "posZ": 9.85, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707105818/DFA0311033860DD6A8E14C0A40B4BD71F2B06697/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707184473/85338D56A64A03F43794AB8EF5F3A5763AE140B1/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e876b4", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Roland Banks", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": true, - "Stretch": true, - "Thickness": 0.1, - "Type": 3 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698230706030974/1E4B02FA9FF5641454726CD0253815C27811BCAE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698230707104892/3E08D3A6B8BE778339739AF6AE2D2E70A8D2877A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "1ca5e3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Roland Banks", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.39903641, - "posY": 0.960003, - "posZ": -7.088509, - "rotX": -0.0000144364685, - "rotY": 180.000015, - "rotZ": -0.0000190262035, - "scaleX": 0.5657107, - "scaleY": 1, - "scaleZ": 0.5657107 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.773, - "posY": 3.032, - "posZ": -0.521, - "rotX": 359, - "rotY": 180, - "rotZ": 2, - "scaleX": 0.57, - "scaleY": 1, - "scaleZ": 0.57 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "177b26", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Core Set", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 14.623, - "posY": 3.277, - "posZ": 6.751, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8b8b41", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Tokens", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.206, - "posY": 1.989, - "posZ": 9.149, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0.70196, - "g": 0.12265, - "r": 0 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632852164/122AAC50A71B7D18F294A9BD89CD7ADCD4137BEC/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632852099/C6688E9A7DCB0639E5EA9A94A933117BE3D91406/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "62c3f3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Lily Chen (Quadwield)", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292681, - "g": 0, - "r": 0.170584649 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632852799/B987558D827908D8E682E56B4E375FB80114B4C9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632852747/E5EE8542C7B2AF711792A707E05B4E897FF91A74/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Lily Chen", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.04929137, - "posY": 1.05605388, - "posZ": -6.07517147, - "rotX": -0.0000128731717, - "rotY": -0.00369770988, - "rotZ": -0.00007737283, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 11.352, - "posY": 3.569, - "posZ": -9.348, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351027159/2DBB796009E69901F3C1D1B8473BB3672936A415/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351027111/EC77E0839D2BF35011E726F6B056C604B1BC0957/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "dc366b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Ursula Downs", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961648, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351206144/BF48660D48BA4D7E209EB1C77314516200863872/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351206096/3EBB1E7FBDB4A0E73B5EDC9270115DC7BD37BCDE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Ursula Downs", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.53544873, - "posY": 1.056051, - "posZ": 1.079134, - "rotX": -0.000013576212, - "rotY": 180.007568, - "rotZ": 0.0000272131547, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.939, - "posY": 3.14, - "posZ": -6.599, - "rotX": 0, - "rotY": 0, - "rotZ": 359, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534350346256/CC27F5FD13BD3C0EE00298D3448F2132B07D2F59/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534350341026/33A1107FCBE63B79954F6233949CCD92075E8168/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8d2a57", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Sister \"of Battle\" Mary", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3257833, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351206055/6177641413E66D4DC5088B9FAC86C421999A50D9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351222568/AA5B1E986CA44C9A16A52D159FB81F2C1C9A3FCF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Sister \"of Battle\" Mary", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.85660493, - "posY": 1.05605114, - "posZ": 1.132718, - "rotX": -0.0000478298753, - "rotY": 359.982635, - "rotZ": -0.00000308779613, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.34, - "posY": 3.123, - "posZ": -6.589, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351095851/70F53C72CA6DE8070E52CA366CBB7B80264BF5B6/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351095787/538D7D90695B47562A96E19B4F47482429CF6EBF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "66a610", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Duke", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329268 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351205900/3CD8AF69B384FB0B45BDDBC6C87748DA201D7C19/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351205842/2D2DF3A1166FB07C46B4265CC67AC3B49381FB6F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Duke", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.66877067, - "posY": 1.05605078, - "posZ": 1.01610041, - "rotX": 0.0000200246959, - "rotY": 0.152080789, - "rotZ": 0.0000106987563, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.186, - "posY": 3.123, - "posZ": -6.705, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351080114/4D8203AF383B01CB7B302EFF49FC629C5AA11146/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351080066/13060798A5F3A894615E557FD8BBA2D687F3B8A2/", - "WidthScale": 0 - }, - "Description": "Duke has been Sacrificed", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bc2b80", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Ashcan\" Pete", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329268 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351205737/06E479385625E40513318FA7B65A3BB43B6C51CE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1786217534351205658/F4C9CEA767EB7EBFFA249DDEBA9961534C4FD0F9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a079d2", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Ashcan\" Pete", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.6126439, - "posY": 1.0560509, - "posZ": 1.097108, - "rotX": -0.00000630444856, - "rotY": 359.845673, - "rotZ": 0.00000182276153, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 14.134, - "posY": 2.868, - "posZ": 2.761, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124480/072CCD17AAD9F44762EFB20DA674642F5D66958C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124438/5F3C01090BFF6D46E19F15A41359A7F8AFC809AF/", - "WidthScale": 0 - }, - "Description": "Novella", - "DragSelectable": true, - "GMNotes": "", - "GUID": "84b0e7", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Norman Withers", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961678, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112131/81797D3C3350D506C21AFE126DEEECC4EDDC574F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112089/1C1F3DC7EC4D5A03C8A5991D1940DD873521B063/", - "WidthScale": 0 - }, - "Description": "Novella", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5332e1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Norman Withers", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.451320738, - "posY": 1.02681971, - "posZ": 4.430932, - "rotX": -0.00006245089, - "rotY": -0.00515922671, - "rotZ": -0.00007256795, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 7.535, - "posY": 3.13, - "posZ": 1.318, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "Alternate Versions of Various Investigators", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5b8959", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Alternatives", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 16.487, - "posY": 1.786, - "posZ": -16.111, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122497/B3DB3D61DCE74E7076DDA6AB6B3FF7DBC6CA5A87/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122453/D6DAFCA7AA289916A6EAAF69E0B08D4B55D57E17/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0a4876", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Duke", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329267859 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110221/4D271515C4BDE28EE6ACAB84479097C6B872F3A1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110177/D5B45888598DE6C803A741BDB2285BEEC1089001/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Duke", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.16754961, - "posY": 1.02681947, - "posZ": 10.9790955, - "rotX": -0.000007267226, - "rotY": 30.0019035, - "rotZ": -0.0000207458615, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.09, - "posY": 3.134, - "posZ": -5.438, - "rotX": 1, - "rotY": 0, - "rotZ": 359, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123015/87E6EBB0756C3E964B6F238DA3E6BBD99881EFCD/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122972/496A614B7D0455E6E1FE7FEB4DFF8C6A6A748C98/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "370dc2", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jacqueline Canine", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292678, - "g": 0, - "r": 0.170584351 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110712/9758DCD708860BB091FB1C11B77D4F8864DB45B0/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110669/A0661C0A60E2112B2AACD1C24396BDD40B89C300/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jacqueline Canine", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.624458, - "posY": 1.02681983, - "posZ": 6.08223963, - "rotX": -0.00007169856, - "rotY": -0.00505916635, - "rotZ": -0.00009209767, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.241, - "posY": 3.14, - "posZ": -5.542, - "rotX": 0, - "rotY": 0, - "rotZ": 359, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124568/678199B54EDC3EF4494A91B1C2178A180213A7BE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124527/8BD07B46684E9D56DBA93454BD516F41313D46C9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b7c253", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Skids\" O'Droole", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800737, - "r": 0.08509361 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112225/1548F58B35A11B751C99FF7D2CA936642BAE301F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112175/10140B73067B90C02617F05014D138D39E76E9B4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Skids\" O'Droole", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.129614055, - "posY": 1.02681983, - "posZ": 6.08450127, - "rotX": -0.0000665585, - "rotY": -0.004955965, - "rotZ": -0.00009775421, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.402, - "posY": 3.143, - "posZ": -5.519, - "rotX": 0, - "rotY": 181, - "rotZ": 359, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125925/26224E51EFC07F9A4359A2BEC39F1AE13A5D5029/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125883/E2B1FFEDE8317BD06F54857DFB745FC7A5672740/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b4eab6", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Kate Winthpup", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961738, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113695/336E479B2D4CFBB13BCF6727A02ABD9213EAB8F5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113660/4DAA98F00C8E46A7A3F4DFA1B9422201EB7C64CC/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Kate Winthpup", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.8642114, - "posY": 1.02681983, - "posZ": 6.082245, - "rotX": -0.0000582093635, - "rotY": -0.00490534632, - "rotZ": -0.00007865141, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.477, - "posY": 3.11, - "posZ": -5.029, - "rotX": 2, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122033/EA0943FD858FD7EF1188ED6363B997D9CFB7F6E4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343121985/B74860D47613A5F110FD86E2D760C660C560347C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c971c3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Bark Harrigan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3257835, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109744/C48F56B459C4D7DA169C75B638C88096C7B0D142/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109705/5CBC385554F9CD0389E2A07743EECD5D35409198/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Bark Harrigan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.6265018, - "posY": 1.02682006, - "posZ": 6.088344, - "rotX": -0.0000672340248, - "rotY": -0.005086284, - "rotZ": -0.00008665504, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.561, - "posY": 3.141, - "posZ": -5.615, - "rotX": 359, - "rotY": 90, - "rotZ": 1, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ffb761", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Barkham Investigators", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 6.274, - "posY": 1.786, - "posZ": -11.9, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.59408, - "g": 0.59408, - "r": 0.59408 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124733/E87E89DE233547010A2303714F65A67CE1599169/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124689/3BD7F394964F51DE2342BB163110083CA9389E0A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45d884", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Penny White", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.162020624, - "g": 0.162020624, - "r": 0.162020624 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112424/E2171B84B8D68AC259E83DB8AF0D8315DED7D34A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112380/E232D3010841C5E757778F8F6D032863DAB2FC9F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b05799", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Penny White", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.21406651, - "posY": 1.02681863, - "posZ": 7.840794, - "rotX": 0.000108280154, - "rotY": 29.945343, - "rotZ": 0.0000630313662, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.943, - "posY": 3.136, - "posZ": -9.118, - "rotX": 1, - "rotY": 255, - "rotZ": 359, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.59408, - "g": 0.59408, - "r": 0.59408 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125728/A01F2688903AF1F172EA8F6E35C197BF080CA3FD/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125669/07A11B3AA9B849680D17A3ACA41C374F29EEB2DF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2b5c11", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Valentino Rivas", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.162020653, - "g": 0.162020653, - "r": 0.162020653 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113519/BADB45B19BB567E328DDEAB0B58B978946B6936B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113479/EA4B290E0CCFB030C9D1B50061A95B1D2A975E88/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5aa59a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Valentino Rivas", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.266295, - "posY": 1.02681923, - "posZ": 9.360523, - "rotX": -0.000060371538, - "rotY": -0.004860382, - "rotZ": -0.0000793984, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.456, - "posY": 3.14, - "posZ": -9.586, - "rotX": 359, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.59408, - "g": 0.59408, - "r": 0.59408 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123355/7A11BAC58DD612E7158C8842C8AFE0BF3D8C90FB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123311/601F1BAC6250D2BE1D2D8C635246778D098FF1C6/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4eebfc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jerome Davids", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.162020624, - "g": 0.162020624, - "r": 0.162020624 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111005/348A8E2F0446D09998C1D72E999C9ED25D992433/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110959/47D5246EE230991A1352CE0743B10168A9ECB586/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a44908", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jerome Davids", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.505696, - "posY": 1.02681923, - "posZ": 9.364632, - "rotX": 0.0000194205968, - "rotY": -0.00489224726, - "rotZ": -8.50184756e-10, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.766, - "posY": 3.116, - "posZ": -9.727, - "rotX": 357, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.59408, - "g": 0.59408, - "r": 0.59408 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122673/7A6A0F30F2D7CF2F8F33548DE0EF94D4443F2952/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122636/CCA770E23817DB041A752079D020341C3392A9B0/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "58e76c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Gavriella Mizrah", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.162020653, - "g": 0.162020653, - "r": 0.162020653 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110415/1F049FB52103B0A63A68EAD64AA4942C290440B9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110365/607CFC0994A129AC8452F0A6FD2BE17609E73573/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "05484e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Gabriella Mizrah", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.74845767, - "posY": 1.02681923, - "posZ": 9.401479, - "rotX": 0.00000519274545, - "rotY": -0.004959853, - "rotZ": 0.0000114189979, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.245, - "posY": 2.94, - "posZ": -8.384, - "rotX": 1, - "rotY": 359, - "rotZ": 1, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "36c3f5", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The Circle Undone Prologue Investigators", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.145, - "posY": 3.239, - "posZ": -16.331, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 0, - "g": 0.36652, - "r": 0.70588 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.59408, - "g": 0.59408, - "r": 0.59408 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126008/5F98DD009140C4BD39E4461593131568BC871913/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125963/C651F00D5A3DEEA70C450B496E271C6708710BE8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "458fc8", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.162020653, - "g": 0.162020653, - "r": 0.162020653 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113771/369BEF14B41868A935F9AE48A36B6A31B3C84112/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113735/A56ABCAA1194E052A0A976A7CF57CCD1D95BA5A4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45eb73", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.66814137, - "posY": 1.02681983, - "posZ": 10.2142458, - "rotX": 0.00007342901, - "rotY": 359.9585, - "rotZ": 0.000199222835, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 16.12, - "posY": 2.462, - "posZ": -5.572, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126008/5F98DD009140C4BD39E4461593131568BC871913/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125963/C651F00D5A3DEEA70C450B496E271C6708710BE8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d4d93e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329267949 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113771/369BEF14B41868A935F9AE48A36B6A31B3C84112/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113735/A56ABCAA1194E052A0A976A7CF57CCD1D95BA5A4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.922006, - "posY": 1.02681768, - "posZ": 10.2471294, - "rotX": 0.0000877302737, - "rotY": 359.958527, - "rotZ": 0.00007899584, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 14.362, - "posY": 2.737, - "posZ": -7.039, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126008/5F98DD009140C4BD39E4461593131568BC871913/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125963/C651F00D5A3DEEA70C450B496E271C6708710BE8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2574ee", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.329267919, - "g": 0, - "r": 0.17058447 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113771/369BEF14B41868A935F9AE48A36B6A31B3C84112/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113735/A56ABCAA1194E052A0A976A7CF57CCD1D95BA5A4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.181207, - "posY": 1.02682018, - "posZ": 10.2294979, - "rotX": 3.72182342e-7, - "rotY": 359.958557, - "rotZ": 0.00000320151662, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.735, - "posY": 3.142, - "posZ": -6.967, - "rotX": 0, - "rotY": 135, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126008/5F98DD009140C4BD39E4461593131568BC871913/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125963/C651F00D5A3DEEA70C450B496E271C6708710BE8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "531452", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800856, - "r": 0.0850936845 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113771/369BEF14B41868A935F9AE48A36B6A31B3C84112/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113735/A56ABCAA1194E052A0A976A7CF57CCD1D95BA5A4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.427137, - "posY": 1.02682054, - "posZ": 10.2317638, - "rotX": -0.0000723652, - "rotY": 359.958527, - "rotZ": -0.00009052689, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.649, - "posY": 3.142, - "posZ": -6.911, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126008/5F98DD009140C4BD39E4461593131568BC871913/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125963/C651F00D5A3DEEA70C450B496E271C6708710BE8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "21dca1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.2519618, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113771/369BEF14B41868A935F9AE48A36B6A31B3C84112/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113735/A56ABCAA1194E052A0A976A7CF57CCD1D95BA5A4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.6925416, - "posY": 1.02682018, - "posZ": 10.2295065, - "rotX": -0.0000595574675, - "rotY": 359.9584, - "rotZ": -0.00009432038, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.917, - "posY": 3.137, - "posZ": -7.03, - "rotX": 0, - "rotY": 0, - "rotZ": 1, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126008/5F98DD009140C4BD39E4461593131568BC871913/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125963/C651F00D5A3DEEA70C450B496E271C6708710BE8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "01d035", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.325783521, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113771/369BEF14B41868A935F9AE48A36B6A31B3C84112/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113735/A56ABCAA1194E052A0A976A7CF57CCD1D95BA5A4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Body of a Yithian", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.91376317, - "posY": 1.02681971, - "posZ": 10.2119522, - "rotX": 0.00028008892, - "rotY": 359.958527, - "rotZ": 0.0002694775, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.192, - "posY": 3.105, - "posZ": -6.389, - "rotX": 1, - "rotY": 0, - "rotZ": 1, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "228120", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "The City of Archives (SPOILERS)", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.849, - "posY": 3.273, - "posZ": -16.44, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343369844/741F46B92FA73AFDB7BBC76C10CC3E5ABD31D4CB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122725/89A2F73B69E58215A310CCB38DD6C4CFC335D072/", - "WidthScale": 0 - }, - "Description": "Novella", - "DragSelectable": true, - "GMNotes": "", - "GUID": "596da1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Gloria Goldberg", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292678, - "g": 0, - "r": 0.170584351 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343370892/71C122A8117FFF40D9874C42BCD6D0F8B8B7892D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110452/64B813104A1823E356C86EE768B4C207D41F159F/", - "WidthScale": 0 - }, - "Description": "Novella", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2919ee", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Gloria Goldberg", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.93999064, - "posY": 1.02681971, - "posZ": 4.430925, - "rotX": -0.00006585491, - "rotY": -0.005149761, - "rotZ": -0.00007726023, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 9.138, - "posY": 2.224, - "posZ": -10.16, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632829693/2DA5CDF37EAB1E351A88995270B9A313DAEA59CE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632829647/900F45FD95C2893461BCAA1572DE0E7EA16ABE19/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d5f368", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Summoned Servitor", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.329268128, - "g": 0, - "r": 0.170584679 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632829093/FBA3DF6AD2DF21A559274FB770ADB98A0368EE0D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632829036/6D4D65B59894195F8047FA6F37F7379A67CDD4E4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Summoned Servitor", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.92249012, - "posY": 1.09781313, - "posZ": -2.00294733, - "rotX": -0.0000188514023, - "rotY": 0.00833301, - "rotZ": -0.0000436505143, - "scaleX": 0.8250001, - "scaleY": 0.8250001, - "scaleZ": 0.8250001 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 16.571, - "posY": 2.109, - "posZ": -14.587, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.83, - "scaleY": 0.83, - "scaleZ": 0.83 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.59408, - "g": 0.59408, - "r": 0.59408 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1814399894374997063/8267261868DBD6C630FAD36CE43AC9F594A23328/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1814399894374997017/BC85365BA8B6FD488A31D0453F5B589E9FBB33A3/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "409e5c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Charlie Kane", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.162020534, - "g": 0.162020534, - "r": 0.162020534 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375044931/1F649E841492469182395800CD8522A7A8F688DF/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375044875/74403D42281F3B6F52977E2A78B3429284D14162/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Charlie Kane", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.88261676, - "posY": 1.05605388, - "posZ": -7.99951649, - "rotX": 0.000172622473, - "rotY": 0.04173309, - "rotZ": -0.000115265633, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 13.213, - "posY": 2.067, - "posZ": -14.178, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632805684/E19E42FA0F5C66C9AE0C531B8502F0856EA77CB4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632805644/FA91A9D835F60BCCAD90EE43F432EEFCB812F1BE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "821043", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Darrell Simmons", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329268128 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632806197/159CC606BB4D245E2432664D0C7BB86D7B3180E5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632806149/6DE55CD4B35E67EDD1EA12561615C29D7528C1EC/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6b2246", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Darrell Simmons", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.66304708, - "posY": 1.05605173, - "posZ": -3.49813342, - "rotX": -0.0000887667, - "rotY": 0.0107405549, - "rotZ": -0.0000118543749, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 23.71, - "posY": 2.067, - "posZ": -11.306, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632804138/3C7683C5652FF748E998DA47244ECD565E47A9F0/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632804057/78FE712C27FC15638C93ED390EA244DC24AD9D80/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "347366", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Amina Zidane", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.329268128, - "g": 0, - "r": 0.170584679 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632804895/36BF0E4DF393735062A1097E8B5DEF0CBFF7FE50/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632804852/FC13F19097AB6B8CFF69D41F181DF5FE3B171487/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c8a797", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Amina Zidane", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.91043854, - "posY": 1.05605221, - "posZ": -3.427179, - "rotX": -0.000122606551, - "rotY": 359.8301, - "rotZ": -0.000013014, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 19.26, - "posY": 2.067, - "posZ": -15.304, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632728920/BB862271FE472CB9A059930312C7E02014C926AA/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632712125/4C9C8B7E2D826AC3F667B25886A10E4E7C404D36/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f90c73", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Kymani Jones", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210801154, - "r": 0.08509395 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632729904/030745C06892D3AE331E9B047F6202B3A834B89D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632729866/6CAF10394017D1E413216A18C7AFB68E4BE4A22E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d72b4f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Kymani Jones", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.205485, - "posY": 1.05605233, - "posZ": -3.2741313, - "rotX": -0.00009738026, - "rotY": -0.00172003009, - "rotZ": -0.00011472436, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 16.442, - "posY": 2.067, - "posZ": -15.188, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632778413/67439F129504FB1AA3A5D888DC4719FEBAF9892C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632778350/325286731544DD374EE07847A9446D2BC94EF1B9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bc327b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Vincent Lee", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251962036, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632803119/29423101B686F98DA4B7493746360CD42D3CE5C0/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632803055/AF8D8F383F92D298133EBA09A34753744E4EF548/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Vincent Lee", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.556041, - "posY": 1.05605245, - "posZ": -3.136945, - "rotX": -0.000110429712, - "rotY": 0.00694853, - "rotZ": -0.000125191262, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 13.056, - "posY": 2.067, - "posZ": -14.739, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815627804095/14FB266940C7F4FA6C89D27D778171D8DC132E4B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815627803889/F70AAF20DE0C636E8188C8ED3567D152BFE34FEF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "9b83f7", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Carson Sinclair", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.32578373, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632822423/C4763118F4519CF03FB07EEC9E8D181987D818CF/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1840305815632822371/481BF5F70A061B239975FA608D6FD6BFEE26D87D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Carson Sinclair", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.154865, - "posY": 1.05605257, - "posZ": -3.15310764, - "rotX": -0.000124932936, - "rotY": 0.109544486, - "rotZ": -0.000132667308, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 24.417, - "posY": 2.067, - "posZ": -10.598, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127498935/83C60889FF4E253896F9877AA5D695B65DE9A41D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127498888/52A7D63128EF8CCABABCCAC25D7A161FDC87AC00/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7ce9d9", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Bob Jenkins", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329268217 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127498806/179CA133C12F46DEC40C21BDED8E51EBC441912D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127498690/7767353FD52F7E52E135E1998911A5AA910FE5B1/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Bob Jenkins", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.0395640954, - "posY": 1.056051, - "posZ": 2.101576, - "rotX": 0.0000102862041, - "rotY": 21.3411922, - "rotZ": 0.0000441833763, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 9.343, - "posY": 2.067, - "posZ": -6.363, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127487724/EA02F95CE17F58859603E9C3342085FFBE7E0420/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127487680/802D3885209166F8C3A68A2BC4E5F1B5DF9DE53B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f58123", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Lily Chen", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292682, - "g": 0, - "r": 0.170584738 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127487606/CCEB12CA653DEB53372368FC4E28FD57A87418BD/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127487506/08986629775B03EF635165A59EAEC2FD21BFBD74/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Lily Chen", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.897596955, - "posY": 1.056051, - "posZ": 2.064113, - "rotX": -0.0000204453227, - "rotY": 23.9014645, - "rotZ": -0.0000159633655, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 11.535, - "posY": 2.067, - "posZ": -2.921, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127476794/837B28F92D4D63B23EF69D5A327A76928DC814A7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127476746/94BE1CD298C8605CEE92CA2E8B0405C206EFCDB4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "638f3a", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Monterey Jack", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210801244, - "r": 0.08509401 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127476683/6C480D8E9E8D03B8CA8154285EDE42312D1FE37B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127476598/64CD439C4B27AF54285ECE0BABDB93C2634E5255/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Monterey Jack", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.73105574, - "posY": 1.05605125, - "posZ": 2.08124447, - "rotX": -0.00000262166122, - "rotY": 23.363678, - "rotZ": -0.000006426918, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.857, - "posY": 2.067, - "posZ": -7.872, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127436051/77E8D22FE4EDE5A94D1CBA705DA40BEB45A1E771/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127435972/2096012C595D1FA26F1D91FC84266EA87FD44C5E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8d186c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Norman Withers", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961946, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127449192/81C48A558BC836CC34ADD31B438468E448CE81C1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127449099/5836DD6175D977C38D7A24FEAA4A45892F99479B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Norman Withers", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.60013986, - "posY": 1.05605114, - "posZ": 2.02835059, - "rotX": -0.0000122742522, - "rotY": 25.2844887, - "rotZ": 0.0000137622574, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 12.732, - "posY": 2.678, - "posZ": -5.012, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127372542/9DFD7E7891356D43184E9D1D80532C00E6C2C8C4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127372136/F93D4DA4D1F8449BCFA91E53342BA39B3FCE54E7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "812e07", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Daniela Reyes", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.325783819, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127384635/76BAC5BCC1AFB3749426C325F6C3D485C7938E50/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1696157607127384593/FA4F02D9F246B22593B426584366C312B382A888/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Daniella Reyes", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -3.57336688, - "posY": 1.056051, - "posZ": 2.11468887, - "rotX": 0.00000348761364, - "rotY": 14.9798584, - "rotZ": 0.00000453148641, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 10.168, - "posY": 2.067, - "posZ": -5.196, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125258/BF68ACA30835D3152A03255FFE394BD1D844A5ED/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125217/B965FA4A228DFD62F5D202E4A6DDBFBAFA1C830D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "abd4b9", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Stella Clark", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329267859 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113025/1272B28219E43CD1BCC990A1883B808A5CA59B1D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112969/8EF9EA86807895AC74C4B2D39C3AE171ED85E94B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Stella Clarke", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.27848661, - "posY": 1.02682006, - "posZ": 7.187135, - "rotX": -0.00000230956311, - "rotY": 0.00765810627, - "rotZ": -0.00000191616482, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.789, - "posY": 2.067, - "posZ": -10.241, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343340459/F0D2B8064964B8252241DB136C794FC1B6FF4B53/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123061/6DA4E0C0B84DED83550ED40969143A242525FEFF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "755af5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jacqueline Fine", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292677, - "g": 0, - "r": 0.170584291 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343339400/E453D38E8FE432F95339B60B4547A59EF8C6BA24/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110754/30A40B1E167B52D90D19922ADF83CBCF1C700638/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jacqueline Fine", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.5499153, - "posY": 1.0268203, - "posZ": 7.169164, - "rotX": -0.00008734602, - "rotY": 0.00341988052, - "rotZ": -0.000114085895, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.768, - "posY": 2.067, - "posZ": -9.959, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125837/45463C0CFEC0E75C4CB0817760252B81E46D4249/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125782/BCC613AFBA7B0DA1250A17504464AAD6B2BAB528/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2ede96", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Winifred Habbamock", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800737, - "r": 0.08509361 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113609/433D67A5C68762077EF84A2BEFF3B251EC84C39E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113571/5531EC31A19AD8DCAD380DA9471B3224001E4E04/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Winifred Habbamock", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.216387287, - "posY": 1.02682042, - "posZ": 7.17177439, - "rotX": -0.0000843822854, - "rotY": 0.007669506, - "rotZ": -0.00011174324, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 10.124, - "posY": 2.112, - "posZ": -7.966, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122928/D4FBD31E1D7FBEEB07939D3DCCB1667E4D1ACA14/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122863/EB70BA317BF67FCF51083EBC0D8C357FF19EAB1B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a0293e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Harvey Walters", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961648, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110630/EBF1D33617A617E04FBD4AE37506F77867222D6C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110588/754FAC695ACFD404B80954EA68E223E454E212D8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Harvey Walters", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.9509883, - "posY": 1.02682018, - "posZ": 7.169526, - "rotX": -0.0000889513249, - "rotY": 0.00762059959, - "rotZ": -0.00009840328, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 9.054, - "posY": 2.067, - "posZ": -8.127, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124386/09CAB97763D97EFC37A1A558CEC032EEB8142754/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124346/CF535E2C85E5F4D1400A96DB7BEDC7DCFC7591C1/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "619a54", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Nathaniel Cho", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3257834, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112047/FB45F4E6B30D69A196C585501C4E75B435677A01/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111987/455AD3904C7CBD6CB4AC917973B766BFACC429DB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Nathaniel Cho", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.713281, - "posY": 1.02682006, - "posZ": 7.175626, - "rotX": -0.000011563644, - "rotY": 0.00344528654, - "rotZ": 0.000006189511, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 7.437, - "posY": 2.067, - "posZ": -7.523, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125121/418C6AC1E5031B49969BD78AE05247AF806FEF1A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125169/A6DCCBFE8E038499BBDBC3E646EA176F47D2BB4C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "080f71", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Silas Marsh", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329267859 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112871/8EDFE0718C49CAE4155F3832571CCA7202D6479A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112916/0BC1B2CB47C8B5814D1EF29AEF132D972F61B509/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Silas Marsh", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.26874578, - "posY": 1.02682, - "posZ": 7.98725033, - "rotX": -0.00009505729, - "rotY": 359.9731, - "rotZ": -0.00012580266, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.718, - "posY": 2.067, - "posZ": -7.328, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122330/B1CA265DC32559781219981BF4F385D423CA0702/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122282/12781D255D67BE8DC5D7DC0319C69B25FE6FE0FB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6dbc60", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Dexter Drake", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292678, - "g": 0, - "r": 0.170584351 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110029/BA54F4CD9A3A11718873A2526C95E5846D8709DC/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109980/118F2314AFF798868F18CC252405A87BD978A967/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Dexter Drake", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.527940869, - "posY": 1.02682, - "posZ": 7.969628, - "rotX": -0.00008174957, - "rotY": 359.973175, - "rotZ": -0.000110224384, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 12.743, - "posY": 2.735, - "posZ": -4.874, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125535/2F16BFC124F43F9940C2A3F171E26DDA6463A212/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125485/4E1E451CA47B9A6C4FA602597948FD6EBC4BE776/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "01631f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Trish Scarborough", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800737, - "r": 0.08509361 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113340/C747576C358B25CEA21F2C6BBC99E958B054C988/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113269/8A30699B302BAF982C8FE26E09705CDFF5242C2C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Trish Scarborough", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.238272026, - "posY": 1.02681971, - "posZ": 7.97210073, - "rotX": -0.00008332766, - "rotY": 359.9732, - "rotZ": -0.0001203627, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 10.923, - "posY": 2.067, - "posZ": -4.616, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343121817/321366F4FBE2B24C8D692FDBF493A6D1B630D8A7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343121778/B9E73495E82666A9087D80DF78DF610AB0CEA287/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ef4a8b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Amanda Sharpe", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961678, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109555/08705215BB0218B22B2D2FBF3D870FE043CBF9DE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109504/E045893755B64DC1954A3EA2AB34212F56D195DC/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Amanda Sharpe", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.960734367, - "posY": 1.02682018, - "posZ": 7.96963739, - "rotX": -0.00008332174, - "rotY": 359.973175, - "rotZ": -0.000118014796, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 9.084, - "posY": 2.067, - "posZ": -4.571, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124131/B28E44DBC3463F011BF01FCD6F2E88CD43057352/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124088/F02DA60FB218E7DBC8EAE29556A4CAD9EC47657D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "707374", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Sister Mary", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.325783432, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111792/58A855A135BF1CB9CC76C2C8282D75F27EDAA53B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111744/D91E9AF17853E7AC5E4C32C098F6E60E4E3B2693/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Sister Mary", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.72302723, - "posY": 1.02682018, - "posZ": 7.975754, - "rotX": -0.000010495859, - "rotY": 359.973053, - "rotZ": 0.000002777137, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 17.971, - "posY": 2.067, - "posZ": -3.934, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124652/C352844E8CE07642B8F0D34FD4CC46FD163099BF/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124612/064FA25C5A7EC3F7A7752FF362462E26C3045BAD/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ef94b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Patrice Hathaway", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329267859 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112332/4D2A00300C1A749C808CD8391F3E681038EDAFDB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112272/1BAC1C83A3683725B46BAF4BD60184BA52BFB6E8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Patrice Hathaway", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.28675163, - "posY": 1.02682006, - "posZ": 8.563183, - "rotX": -0.0000178168229, - "rotY": 0.09771161, - "rotZ": -0.0000015089422, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 9.221, - "posY": 2.108, - "posZ": -9.515, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123788/B66846378167F716F4EC90D96FF72354A46B7B4D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123743/7FFD073A2FE1B6346E43939B2C30CC485D4773E9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a908ec", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Luke Robinson", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.32926777, - "g": 0, - "r": 0.170584321 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111421/31C173F4DC2F80612AC7B8A56E9ABD5581EEFFFB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111373/B5CF244E06B8E75CB81575C3E460E11388C3BE15/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Luke Robinson", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.545947134, - "posY": 1.02682018, - "posZ": 8.54556751, - "rotX": -0.00009203844, - "rotY": 0.09762085, - "rotZ": -0.000135061448, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 7.614, - "posY": 2.067, - "posZ": -9.75, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125442/09C00F6166ED8C161E8B4EDC8D6C5712BE4E5A99/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125399/E8846343EE7FB81463CBA96EE1FB4EA48783478D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8788d0", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Tony Morgan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800737, - "r": 0.08509361 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113211/50B0632176C34F7C87676B08F33D1FD47B81F866/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113164/306E90DAA180AD468A2984E850C930EE18CF2662/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Tony Morgan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.208120644, - "posY": 1.02682, - "posZ": 8.547819, - "rotX": -0.00000545394323, - "rotY": 0.09764598, - "rotZ": -0.00000277906065, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 6.206, - "posY": 2.067, - "posZ": -9.638, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123877/541D16A7F8B1CD918142D5ED9481A0AD37271E7B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123832/EF70CBEF33BC038D502B78BA9A77AB07D581B30D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0d602e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Mandy Thompson", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961678, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111511/9DE040757FEB58FB6F7E89805EDBCABA28C08363/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111464/EB5F444E471B86E0CFD56020F3DBC31FF73AECF6/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Mandy Thompson", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.9427243, - "posY": 1.02682018, - "posZ": 8.545574, - "rotX": -0.000108375389, - "rotY": 0.0977096856, - "rotZ": -0.000105106228, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.73, - "posY": 2.067, - "posZ": -9.555, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125343/F146511CB6A21254B2C7350694A25207323BD032/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125301/A93BD669C731E917A35FA5738EB136BF6671D77D/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bea47e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Tommy Muldoon", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.325783432, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113125/C886BBE085401CBDEDC79316126DAD04C160290D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113075/3A5F451FE5EEA0438A7BF83AA80087D508A95E63/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Tommy Muldoon", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.70501757, - "posY": 1.02681828, - "posZ": 8.551668, - "rotX": 0.000446396472, - "rotY": 0.09762672, - "rotZ": 0.00008202271, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 10.5, - "posY": 2.114, - "posZ": -7.323, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123948/0EE225E2455C4D60FC903AC34D5DCBE3A96E0F6E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123913/0FED2F578A1A3B0D7068AFA723C5BD86614D51AE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd52c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Marie Lambeau", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.329267919, - "g": 0, - "r": 0.17058447 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111601/E3DB7C44A8A034D4BA7AACF2599A54DECC4C4D34/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111548/9D1687057358C422472126D059B5CAA1828E2DEA/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "00f638", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Marie Lambeau", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.99495029, - "posY": 1.02681935, - "posZ": 9.381206, - "rotX": -0.00000478800757, - "rotY": -0.004909135, - "rotZ": 0.0000121728181, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 8.613, - "posY": 2.067, - "posZ": -7.518, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125001/48ACCE90A8C1A957EC54B0B411DFCCE5F96E1278/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124955/B83959B059ED90771F6550DF20D774B6AA7B088B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c46146", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Rita Young", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329267949 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112745/522D275483FAF39565F28DDDBD405D585F0C23BD/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112692/3353B6C8A7FBFBA7B834CCAF8983B69C054DF2CB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Rita Young", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.22370946, - "posY": 1.02681935, - "posZ": 9.402242, - "rotX": -0.00007423215, - "rotY": -0.004866944, - "rotZ": -0.00008046501, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 7.263, - "posY": 2.067, - "posZ": -7.433, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122410/6B2888110D0DEBD972B1AABC1493F262DB074311/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122370/E81C333FD7195F674DEA2E36024F4788B51472AA/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2640ba", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Diana Stanley", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292679, - "g": 0, - "r": 0.17058444 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110130/FBBAE9140E940B1875BB8141D84788EF5EA5E25C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110082/76D9557C29556247A925A3526A32EB896C0915C5/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Diana Stanley", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.4829111, - "posY": 1.02681768, - "posZ": 9.384614, - "rotX": 0.00032247862, - "rotY": -0.005024162, - "rotZ": 0.0000656196353, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 6.084, - "posY": 2.067, - "posZ": -7.082, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124816/08D840B61C1E9CD93C1720AD66D780B1D7326D10/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124775/8093BCC3BB3D19C324B59EA46C83293B09B3C736/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6e99ba", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Preston Fairmount", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800856, - "r": 0.0850936845 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112537/AE38E517BE326158E67760C17DD7BDF24BDD6C12/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112489/897B3C993413B04F475D3AD95385262A0188945C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Preston Fairmount", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.2711577, - "posY": 1.02681839, - "posZ": 9.386873, - "rotX": 0.000328625174, - "rotY": -0.004782497, - "rotZ": 0.000219832931, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 12.619, - "posY": 2.258, - "posZ": -4.266, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123531/ADE4594F7EACB7EE9E942D3477724E674AE1AADE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123488/AB5C5C45431B8CF33A784A09E265D60D4D0168CC/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bc0aa2", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Joe Diamond", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.2519618, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111172/100EC490B6AC901F4AFC48F100829575055BEE86/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111123/FFF20A721CE1B8D3C49E32DBC0A7861229012FCE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Joe Diamond", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.005756, - "posY": 1.02681589, - "posZ": 9.384623, - "rotX": 0.000161581018, - "rotY": -0.00500129443, - "rotZ": 0.00012048176, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 11.571, - "posY": 2.067, - "posZ": -4.393, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122241/ACA5D39ABF796E8BFEC57DB81EA70B8558194C3B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122187/0A3F1BFDDD7476762C6645E64A9CBDB74E0BE4BB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "99e9da", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Carolyn Fern", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.325783551, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109932/062A9610BF91F9E900D727CF4C278508341AD4E0/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109894/23141F07C9E777CCA48F96486D98A8D4DAD0447E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Carolyn Fern", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.76804483, - "posY": 1.02681708, - "posZ": 9.390722, - "rotX": -0.00002144888, - "rotY": -0.004910287, - "rotZ": -0.000050051578, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 8.8, - "posY": 2.067, - "posZ": -4.619, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122138/C6DA1F44C07BF2315BEF34CE88BE2D37306D72B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122092/7E86DE74F323125CF4C743A41D5C38CD7E5E6F80/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "00a0b3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Calvin Wright", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329267949 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109838/295776AE6C4195E36179BFD4DC840A85F9DC5E2A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109794/51E0C9D150A28AE12DBC44D081B33F8585CA252A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "db5e16", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Calvin Wright", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.19241536, - "posY": 1.026818, - "posZ": 10.2151451, - "rotX": 0.00043980364, - "rotY": -0.001790508, - "rotZ": 0.00007277957, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.844, - "posY": 2.067, - "posZ": -5.017, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343213159/D07849717C07B630C290BB7810B30108A652DC52/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343213115/923ED3CD7BAB52379528C97BA86565BC972937F2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "037a27", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Father Mateo", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292679, - "g": 0, - "r": 0.17058444 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343213075/853E2B0B3A7ACD28E70E4DA3CD2347E714923AC3/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343213028/571662D666A8333EC73EA50FA1FC52121F182D33/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Father Mateo", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.451427639, - "posY": 1.02681625, - "posZ": 10.1980467, - "rotX": 0.00000425053668, - "rotY": -0.00196357, - "rotZ": -0.000004695836, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 15.605, - "posY": 2.843, - "posZ": 0.227, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122592/94D7B9AA767123BF2B50B1E27FAC62242DC4C1B3/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343122544/131B9D90BBADDD3576CD4F24E88B1A9EAA084B25/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "259257", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Finn Edwards", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800767, - "r": 0.08509364 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110324/4B8B05471D1F5CC0FDD294EF20DB32F386D4E59E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110275/E2025B1C14EE35E63D44BC3B5A2410B21C397D90/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Finn Edwards", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -2.869375, - "posY": 1.05605507, - "posZ": -6.68307352, - "rotX": 0.00002060866, - "rotY": 0.0192703716, - "rotZ": -0.000007155507, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.672, - "posY": 2.067, - "posZ": -7.577, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125624/CBE958AE77C62FC1CC2DF6663226EADDA1500B5E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125580/D00CFB3B16884A01FA999E0DDD6B3CBC08E65AEA/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "556138", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Ursula Downs", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961768, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113425/FC55EAACC9980F12F2108FB677C8E4F98FDBC82F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113388/3C14FD026614E265AF28D706E14EE28832B9900A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Ursula Downs", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.03705025, - "posY": 1.02681637, - "posZ": 10.1975241, - "rotX": -0.00000102660943, - "rotY": -0.00209664949, - "rotZ": -0.0000199569458, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 20.784, - "posY": 2.067, - "posZ": -0.276, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123611/134B3903A912E19F89379A3CAFC11B690B0F1B6B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123575/9D875BB066BBC8711A4A5B1888E6F14453B937B8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f47f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Leo Anderson", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.325783521, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111248/605DA9FD3CAA488E2AF00196B4BE09173C7690F2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111208/2C442A20E3C1D6C78115F52739C30026DEA42F78/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Leo Anderson", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.79934168, - "posY": 1.02681637, - "posZ": 10.2036343, - "rotX": -0.0000256951953, - "rotY": -0.002032134, - "rotZ": -0.0000530376055, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.806, - "posY": 2.067, - "posZ": -7.253, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.59408, - "g": 0.59408, - "r": 0.59408 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375141540/7C5FA11A32DB50D7C3288D7672AA72A753941813/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375141491/FC32B68CBC5AD09DB7347FDD68F6B398FB52F84A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "392b57", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Lola Hayes", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.162020475, - "g": 0.162020475, - "r": 0.162020475 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375156686/CF8D3AAA42F6F3879C9C79C9E78A696592FE57BD/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1814399894375156637/11194212FD04FF4B2AFAFB25AF915F288D768BEC/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Lola Hayes", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.93965089, - "posY": 1.05605161, - "posZ": -1.30115461, - "rotX": 0.0000733440538, - "rotY": 359.8577, - "rotZ": 0.0000312846, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 6.549, - "posY": 2.067, - "posZ": -0.995, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126093/F7F02472038CC02E52533381E4D7AD7D31BB3FA8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343126049/63E0BC57F7E5141690FF1F50002837F2869FE3FA/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "26a0a1", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "William Yorick", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329268 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113849/DCA3D076BFCA28D8A8638B6A4F638AA84101624E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343113810/78D2AA9C325055A8D4680125D29489A8B9B89461/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "William Yorick", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.21389389, - "posY": 1.02681673, - "posZ": 11.0401068, - "rotX": 0.0000166332375, - "rotY": 359.9848, - "rotZ": 0.00000138512064, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.985, - "posY": 2.067, - "posZ": -5.783, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343160099/FCE303FB3866184962E30F7245071A06425095E2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343160053/79D9A0D7E71BB628AD26366761558E96AACA93CC/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "073478", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Akachi Onyele", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.329267979, - "g": 0, - "r": 0.17058453 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343159970/075B54C1767060DD9A1AD24DB57938CCFDDD6585/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343159910/4DFE2B07BE7E50D2EA69FFEB40624CF542B5E0DE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Akachi Onyele", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.472944528, - "posY": 1.02681673, - "posZ": 11.0225353, - "rotX": 0.0000102060794, - "rotY": -0.0008168299, - "rotZ": -0.00000544288, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.47, - "posY": 2.067, - "posZ": -5.728, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125078/A86918CF754A49F3634CE1906F06D2EF8050181C/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343125039/358B5898D369D0DD9B43CA63A4D814DD523B46D4/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "086503", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Sefina Rousseau", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.210800946, - "r": 0.0850937739 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112830/2D14AED1F1BA6A9FA28E7093A2E738876EA80A89/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112788/E93E0A9A75FF8657A9E42C3AC93A236739E8B48E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Sefina Rousseau", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.2811205, - "posY": 1.02681625, - "posZ": 11.0247946, - "rotX": 0.0000156008318, - "rotY": -0.000685103063, - "rotZ": -0.0000113126, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.353, - "posY": 2.067, - "posZ": -5.625, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124306/A4481703DBC3C327CBE907E1149598283B7AB049/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124265/92345CAE764C3A809A9CA73F30AFF9406BC0EF5E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "1ebf3c", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Minh Thi Phan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.2519619, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111950/D44CE23B259F5B04CD2B76F44DF2BED5D41F862E/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111913/23E26016E556988D267EDD8DEB4D76E4D6D38A7C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Minh Thi Phan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.015713, - "posY": 1.02681613, - "posZ": 11.0225344, - "rotX": 0.000008437017, - "rotY": -0.0006353474, - "rotZ": -0.0000029882583, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.898, - "posY": 2.067, - "posZ": -3.646, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124038/5DB2BE15B15ECF1B8324CCB4EA57762A5F9503D7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123987/395CFF93D01FE8D3E05BF0E18AF363C95F8C9870/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "eb793b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Mark Harrigan", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3257836, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111695/C7C4A01A23F187ADD085F1FDFF51FAFB0295F22A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111650/DC806BD08F8B210C3DBE0D3BE9C0A0E9819D8D54/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Mark Harrigan", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.77800012, - "posY": 1.02681637, - "posZ": 11.0286446, - "rotX": 0.0000544165632, - "rotY": -0.0006005508, - "rotZ": 0.0000232658222, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.73, - "posY": 2.067, - "posZ": -3.996, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343121899/0330056F163A97214EBEDBDA800BF186EEC0B8B7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343121862/DD3C4ACBC52C4FBD8C573B953ED0081B810FA878/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "78f659", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Ashcan\" Pete", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.3292681 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109656/54EBB31786FD3F4119F15B55B4FFAD9108FB9C61/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343109608/01B9628C63EE04370742D354B11FC41708BB6C5A/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Ashcan\" Pete", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.204208, - "posY": 1.02681661, - "posZ": 11.8004837, - "rotX": 0.000008295096, - "rotY": -0.002050401, - "rotZ": -0.0000110992141, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 21.191, - "posY": 2.067, - "posZ": -2.527, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123446/B7327A472FBB4072DC477381E8A7A6125C99531F/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123404/7C7BBACF49827ED5D622033E6C1DEA9357096C7B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4ed067", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jim Culver", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3292681, - "g": 0, - "r": 0.170584649 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111086/FF3A031718F35B572D6C3C8C7BCCAB6C11D4BF33/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343111046/87284B158274F1722C2161D9F9B30E214647B1BB/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jim Culver", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.463523775, - "posY": 1.02681673, - "posZ": 11.7824306, - "rotX": -0.00000354734016, - "rotY": 0.00121953932, - "rotZ": -0.00000394873632, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 0.419, - "posY": 2.067, - "posZ": -3.644, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123260/B4FC280EE97A57E792694AC4EDAADD48E44519C9/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343123208/8C29DCE72C2A5DE4D7E6A79046CF24C3D74D8535/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4990c5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jenny Barnes", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.2108011, - "r": 0.08509388 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110916/B7228A20C559122AB8D718C0AC201FED9E85B2DE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343110878/B9FEDC01D8D9AE1AAC6B02BE02B2750BF3DF54D6/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "9ee7cf", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Jenny Barnes", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.290638775, - "posY": 1.02681673, - "posZ": 11.7851191, - "rotX": 0.0000127248541, - "rotY": -0.0003152209, - "rotZ": -0.00000757728048, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 5.699, - "posY": 2.067, - "posZ": -1.237, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124908/94730221F433510D8906BBF22D7AC5F8FF979B7A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343124866/20FAFD12A141BEDFA62FC7D2BBA10925863110C7/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f96c49", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Rex Murphy", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251961976, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112648/D27F29716439A7BDBF1162AB92684C32E3A221FF/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343112602/C78D725F74D849D4C81D09D1C9B808D1F192A6B8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "871891", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Rex Murphy", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.02522957, - "posY": 1.02681625, - "posZ": 11.78287, - "rotX": 0.000011895655, - "rotY": -0.0009831832, - "rotZ": -0.00000640340477, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 4.395, - "posY": 2.067, - "posZ": -1.549, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343104997/E66355B1514163DDC8E50927F54C6C07DE63EC00/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343104943/9E4F82A799BEF14E1BEC498937F2EB3BB83F6ADE/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e6d68f", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Zoey Samaras", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.3257837, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343107739/4F5A5EE5CF79DAF68B538EEF30DB8DACC10A3565/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400343107696/1E492CCA51ADB35F95FFF64ABBB61765E50B2B26/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "50a1c5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Zoey Samaras", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.78751993, - "posY": 1.026817, - "posZ": 11.7889805, - "rotX": 0.0000125525485, - "rotY": 0.00384989637, - "rotZ": 0.000006301981, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 3.422, - "posY": 2.067, - "posZ": -1.246, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.58711 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898498/10F99CF16FEBE1CD39635ABFF90F805BF5D54918/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898443/A3C543770A98A9D55B8BF1E51B2EE143A42AE1A2/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8f5e71", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Wendy Adams", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.329268247 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911709/5143ADC17BEA16129B1533BE77EE79BF0BD347A5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911659/B0D1767C1391FEAFC991695B614304A626FCEB2C/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b848a3", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Wendy Adams", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 2.99075246, - "posY": 1.05605435, - "posZ": -11.0904865, - "rotX": -0.0000112616681, - "rotY": 0.0209468454, - "rotZ": -0.00006328795, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 18.214, - "posY": 2.067, - "posZ": 20.422, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.65331, - "g": 0, - "r": 0.33758 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898124/10924A4D4866357782DFB81FFB8D67C93F064F29/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898077/CB788C381652994294C25179935B1C9A43958EF9/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "881def", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Agnes Baker", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.329268247, - "g": 0, - "r": 0.1705848 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911264/8125422EA13893015B535ECA5730368BC422A508/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911213/3C50F1305A311942B242F56F1DF8DB6333F02EC0/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c646b5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Agnes Baker", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 1.41371632, - "posY": 1.05605292, - "posZ": -10.7802191, - "rotX": -0.0000142807457, - "rotY": 359.978119, - "rotZ": 0.0001414089, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 18.125, - "posY": 2.067, - "posZ": 17.111, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.43031, - "r": 0.17436 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898401/3200CDC30301B8A58B6CE31AE9BF06DF7BE59B46/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898355/9F2A2046E154557C9D35BC392DA05BCF7A5D99BF/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "47cbdc", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Skids\" O'Toole", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.2108013, - "r": 0.08509406 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911607/D91CC56CCF47669B649D0AB61B12957FBB790C06/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911559/E9A1944160938CAC731E6ECF4E86C8A43DE4BB9E/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c7119b", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "\"Skids\" O'Toole", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -0.120323889, - "posY": 1.05605209, - "posZ": -10.5111914, - "rotX": 0.0000164129942, - "rotY": 0.0260009225, - "rotZ": -0.000002237311, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 18.043, - "posY": 2.067, - "posZ": 18.03, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.45725, - "r": 1 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898199/50F1F2731EA74D362AFAD8157D040D2C28537FC4/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898160/BD419A43A39F1AAE574C3ED1E6BDCA059662526B/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7de2d5", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Daisy Walker", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.251962155, - "r": 0.559233367 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911376/6F57094F6DE7748A2FECE162A8360433100F6A1A/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911313/35AACEA1926713934B02DED9A74B78FFC3CE9883/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "376b04", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Daisy Walker", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -4.413311, - "posY": 1.02681649, - "posZ": 12.1634035, - "rotX": 0.0000590366581, - "rotY": 0.00331070554, - "rotZ": 3.21872079e-7, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 17.642, - "posY": 2.067, - "posZ": 21.115, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 0.04089, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898308/F1D708F76DF5800CE765EAFA0DC09C26A74FF16B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342898253/1379499E10EFB7E14365E7DB265FDEA18852D1D5/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d68f71", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Roland Banks", - "Snap": false, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.325783879, - "g": 0.0107875289, - "r": 0 - }, - "CustomImage": { - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911484/BCB0DA6F1D18C17D45F72A530A648BEC00C54205/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1763698400342911431/4F699092E6EA90370C6AC757DB37FF9D9A6A7FB8/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b49b1e", - "Grid": false, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Figurine_Custom", - "Nickname": "Roland Banks", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -5.15518236, - "posY": 1.02681625, - "posZ": 12.1229219, - "rotX": -0.0000345639019, - "rotY": 359.971771, - "rotZ": -0.00006146331, - "scaleX": 0.5750004, - "scaleY": 0.5750004, - "scaleZ": 0.5750004 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 18.503, - "posY": 2.067, - "posZ": 21.057, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.58, - "scaleY": 0.58, - "scaleZ": 0.58 - }, - "Value": 0, - "XmlUI": "" - } - ], - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "74d499", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Bag", - "Nickname": "Stands", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -1.249, - "posY": 1.987, - "posZ": 1.393, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - } - ], "CustomMesh": { "CastShadows": true, "ColliderURL": "", @@ -44229,11 +24896,11 @@ "MaterialIndex": 3, "MeshURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/tuckbox_h_MSH.obj", "NormalURL": "", - "TypeIndex": 6 + "TypeIndex": 0 }, - "Description": "", + "Description": "by Lemmingrad", "DragSelectable": true, - "GMNotes": "", + "GMNotes": "extras/arkham_fantasy_minicards.json", "GUID": "e17c9e", "Grid": true, "GridProjection": false, @@ -44242,12 +24909,10 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", - "MaterialIndex": -1, "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", + "Name": "Custom_Model", "Nickname": "Arkham Fantasy - Pixel Art Mini-Cards", "Snap": true, "Sticky": true, @@ -47378,358 +28043,6 @@ "Value": 0, "XmlUI": "" }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": false, - "Thickness": 0.1, - "Type": 1 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/1634201755654309873/9A23829955A98CBAC1E6BA2D3E14D4FFF0A86463/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1634201755654309427/59F903E0AF5599D782B756AB92B5D9203002DF61/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bc81cb", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Double-Sided Resource", - "Snap": false, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 29.9, - "posY": 4.095, - "posZ": -21.013, - "rotX": 0, - "rotY": 0, - "rotZ": 359, - "scaleX": 0.46, - "scaleY": 1, - "scaleZ": 0.46 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.0083, - "r": 0.10624 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.2, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/933819604050849524/797528309EFBAC7485283048AAB9DA68B8A31891/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/933819604050849085/9E22AFD7B0157140FC177DBCCBCB1D61D6A0329F/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b171c8", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\n -- Add a button to the object\n local params = {}\n params.click_function = 'toPhaseTwo'\n params.function_owner = self\n params.tooltip = '1. Mythos Phase\\n\\n 1.1 Round begins. Mythos phase begins.\\n\\n 1.2 Place 1 doom on the current agenda.\\n\\n 1.3 Check doom threshold.\\n\\n 1.4 Each investigator draws 1\\n encounter card.\\n\\n\u003e PLAYER WINDOW \u003c\\n\\n 1.5 Mythos phase ends.'\n params.width = 600\n params.height = 600\n self.createButton(params)\nend\n\nfunction toPhaseTwo()\n self.setState(2)\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Descriptive Phase Tracker", - "Snap": true, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.0009864086, - "g": 0.113237955, - "r": 0.04146277 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.2, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/933819604050886219/5987AB68E0D2609CC3831F1311E9070D7189FBA8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/933819604050885611/845B5AA915F30492B5F34864698B9C3627FA5763/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e75551", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "function onLoad()\n -- Add a button to the object\n local params = {}\n params.click_function = 'toPhaseThree'\n params.function_owner = self\n params.tooltip = '2. Investigation Phase\\n\\n 2.1 Investigation phase begins.\\n\\n\u003e PLAYER WINDOW \u003c\\n\\n 2.2 Next investigator’s turn begins.\\n\\n\u003e PLAYER WINDOW \u003c\\n\\n 2.2.1 Active investigator may take\\n an action, if able. If an action\\n was taken, return to previous\\n player window. If no action was\\n taken, proceed to 2.2.2.\\n\\n 2.2.2 Investigator’s turn ends.\\n If an investigator has not yet\\n taken a turn this phase, return\\n to 2.2. If each investigator has\\n taken a turn this phase,\\n proceed to 2.3.\\n\\n 2.3 Investigation phase ends.'\n params.width = 600\n params.height = 600\n self.createButton(params)\nend\n\nfunction toPhaseThree()\n self.setState(3)\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Descriptive Phase Tracker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -39.1360855, - "posY": 1.24541819, - "posZ": 49.92741, - "rotX": 0.0208078064, - "rotY": 269.9888, - "rotZ": 0.016764963, - "scaleX": 1.5, - "scaleY": 1, - "scaleZ": 1.5 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0, - "r": 0.235189646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.2, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/982233321870235526/32D11CE67CBFB6E1197E540F9CA08F871A500C85/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/982233321870235122/492996D07ABF6DDA4B605A3013C4892839DCF1F3/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "042d56", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "function onLoad()\n -- Add a button to the object\n local params = {}\n params.click_function = 'toPhaseFour'\n params.function_owner = self\n params.tooltip = '3. Enemy Phase\\n\\n 3.1 Enemy phase begins.\\n\\n 3.2 Hunter enemies move.\\n\\n\u003e PLAYER WINDOW \u003c\\n\\n 3.3 Next investigator resolves\\n engaged enemy attacks. If an\\n investigator has not yet\\n resolved enemy attacks this\\n phase, return to previous\\n player window. After final\\n investigator resolves engaged\\n enemy attacks, proceed to\\n next player window.\\n\\n\u003e PLAYER WINDOW \u003c\\n\\n 3.4 Enemy phase ends.'\n params.width = 600\n params.height = 600\n self.createButton(params)\nend\n\nfunction toPhaseFour()\n self.setState(4)\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Descriptive Phase Tracker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -39.1360855, - "posY": 1.24541819, - "posZ": 49.92741, - "rotX": 0.0208078064, - "rotY": 269.9888, - "rotZ": 0.016764963, - "scaleX": 1.5, - "scaleY": 1, - "scaleZ": 1.5 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0, - "g": 0.3496283, - "r": 0.5313587 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.2, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/982233321870237827/81374325F650673C37C89E4E2A1DC25F1C97FED8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/982233321870237261/C287CAED2423970F33E72D6C7415CBEC6794C533/", - "WidthScale": 0 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "44077a", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "function onLoad()\n -- Add a button to the object\n local params = {}\n params.click_function = 'toPhaseOne'\n params.function_owner = self\n params.tooltip = '4. Upkeep Phase\\n\\n 4.1 Upkeep phase begins.\\n\\n\u003e PLAYER WINDOW \u003c\\n\\n 4.2 Reset actions.\\n\\n 4.3 Ready each exhausted card.\\n\\n 4.4 Each investigator draws 1\\n card and gains 1 resource.\\n\\n 4.5 Each investigator checks\\n hand size.\\n\\n 4.6 Upkeep phase ends.\\n Round ends.'\n params.width = 600\n params.height = 600\n self.createButton(params)\nend\n\nfunction toPhaseOne()\n self.setState(1)\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Descriptive Phase Tracker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -39.1360855, - "posY": 1.24541819, - "posZ": 49.92741, - "rotX": 0.0208078064, - "rotY": 269.9888, - "rotZ": 0.016764963, - "scaleX": 1.5, - "scaleY": 1, - "scaleZ": 1.5 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 43.122, - "posY": 2.338, - "posZ": -36.618, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.5, - "scaleY": 1, - "scaleZ": 1.5 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 0.339915335, - "g": 0.507659256, - "r": 0.7222887 - }, - "SpecularIntensity": 0.4, - "SpecularSharpness": 7 - }, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/254843371583187306/6844B833AD55B9A34095067B201B311E1348325F/", - "MaterialIndex": 2, - "MeshURL": "http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/", - "NormalURL": "", - "TypeIndex": 0 - }, - "Description": "Include this in custom content for clue spawning!", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2547b3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/CustomDataHelper\")\nend)\n__bundle_register(\"core/CustomDataHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[\nKnown locations and clues. We check this to determine if we should\natttempt to spawn clues, first we look for \u003cLOCATION_NAME\u003e_\u003cGUID\u003e and if\nwe find nothing we look for \u003cLOCATION_NAME\u003e\nformat is [location_guid -\u003e clueCount]\n]]\nLOCATIONS_DATA_JSON = [[\n{\n \"San Francisco\": {\"type\": \"fixed\", \"value\": 1, \"clueSide\": \"back\"},\n \"\tArkham\": {\"type\": \"perPlayer\", \"value\": 1, \"clueSide\": \"back\"},\n \"Buenos Aires\": {\"type\": \"fixed\", \"value\": 2, \"clueSide\": \"back\"},\n \"\tLondon\": {\"type\": \"perPlayer\", \"value\": 2, \"clueSide\": \"front\"},\n \"Rome\": {\"type\": \"perPlayer\", \"value\": 3, \"clueSide\": \"front\"},\n \"Istanbul\": {\"type\": \"perPlayer\", \"value\": 4, \"clueSide\": \"front\"},\n \"Tokyo_123abc\": {\"type\": \"perPlayer\", \"value\": 0, \"clueSide\": \"back\"},\n \"Tokyo_456efg\": {\"type\": \"perPlayer\", \"value\": 4, \"clueSide\": \"back\"},\n \"Tokyo\": {\"type\": \"fixed\", \"value\": 2, \"clueSide\": \"back\"},\n \"Shanghai_123\": {\"type\": \"fixed\", \"value\": 12, \"clueSide\": \"front\"},\n \"Sydney\": {\"type\": \"fixed\", \"value\": 0, \"clueSide\": \"front\"}\n}\n]]\n\n\nPLAYER_CARD_DATA_JSON = [[\n{\n \"Tool Belt\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 2\n },\n \"Tool Belt (3)\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 4\n },\n \"Yithian Rifle\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 3\n },\n \"xxx\": {\n \"tokenType\": \"resource\",\n \"tokenCount\": 3\n }\n}\n]]\n\nHIDDEN_CARD_DATA = {\n \"Unpleasant Card (Doom)\",\n \"Unpleasant Card (Gloom)\",\n \"The Case of the Scarlet DOOOOOM!\"\n}\n\nLOCATIONS_DATA = JSON.decode(LOCATIONS_DATA_JSON)\nPLAYER_CARD_DATA = JSON.decode(PLAYER_CARD_DATA_JSON)\n\nfunction onload(save_state)\n local playArea = getObjectFromGUID('721ba2')\n playArea.call(\"updateLocations\", {self.getGUID()})\n local playerMatWhite = getObjectFromGUID('8b081b')\n playerMatWhite.call(\"updatePlayerCards\", {self.getGUID()})\n local playerMatOrange = getObjectFromGUID('bd0ff4')\n playerMatOrange.call(\"updatePlayerCards\", {self.getGUID()})\n local playerMatGreen = getObjectFromGUID('383d8b')\n playerMatGreen.call(\"updatePlayerCards\", {self.getGUID()})\n local playerMatRed = getObjectFromGUID('0840d5')\n playerMatRed.call(\"updatePlayerCards\", {self.getGUID()})\n local dataHelper = getObjectFromGUID('708279')\n dataHelper.call(\"updateHiddenCards\", {self.getGUID()})\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Custom Data Helper", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 30.163, - "posY": 4.157, - "posZ": -21.518, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.5, - "scaleY": 0.5, - "scaleZ": 0.5 - }, - "Value": 0, - "XmlUI": "" - }, { "AltLookAngle": { "x": 0, @@ -47766,7 +28079,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/UnderworldMarketHelper\")\nend)\n__bundle_register(\"accessories/UnderworldMarketHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nfunction onload(savedData)\n revealCardPositions = {\n Vector(3.5, 0.25, 0),\n Vector(-3.5, 0.25, 0)\n }\n\n revealCardPositionsSwap = {\n Vector(-3.5, 0.25, 0),\n Vector(3.5, 0.25, 0)\n }\n\n self.createButton({\n label = 'Underworld Market\\nHelper',\n click_function = \"none\",\n function_owner = self,\n position = {0,-0.1,-1.6},\n height = 0,\n width = 0,\n font_size = 145,\n font_color = {1,1,1}\n })\n\n hiddenCards = 10\n hiddenCardLabel = '-----'\n\n isSetup = false\n movingCards = false\n\n self.addContextMenuItem('Reset helper', resetHelper)\n\n if savedData ~= '' then\n local loaded_data = JSON.decode(savedData)\n hiddenCards = loaded_data.saved_hiddenCards\n\n isSetup = true\n refreshButtons()\n end\nend\n\nfunction onSave()\n return JSON.encode({\n saved_hiddenCards = hiddenCards\n })\nend\n\nfunction onObjectEnterContainer(container, object)\n if container ~= self then return end\n\n if isSetup and object.tag == \"Card\" then\n refreshButtons()\n end\n\n if object.tag == \"Deck\" then\n if validateDeck(object) then\n takeDeckOut(object.getGUID(), self.getPosition() + Vector(0, 0.1, 0))\n refreshButtons()\n \n isSetup = true\n end\n elseif object.tag ~= \"Card\" then\n broadcastToAll(\"The 'Underworld Market Helper' is meant to be used for cards.\", \"White\")\n end\nend\n\nfunction onObjectLeaveContainer(container, object)\n if container ~= self then return end\n \n if isSetup then\n refreshButtons()\n end\nend\n\nfunction validateDeck(deck)\n if deck.getQuantity() ~= 10 then\n print('Underworld Market Helper: Deck must include exactly 10 cards.')\n return false\n end\n\n local illicitCount = 0\n\n for _, card in ipairs(deck.getObjects()) do\n decodedGMNotes = JSON.decode(card.gm_notes)\n\n if decodedGMNotes ~= nil and string.find(decodedGMNotes.traits, \"Illicit\", 1, true) then\n illicitCount = illicitCount + 1\n end\n end\n\n if illicitCount ~= 10 then\n print('Underworld Market Helper: Deck must include 10 Illicit cards.')\n return false\n end\n\n return true\nend\n\nfunction refreshButtons()\n local cardsList = ''\n\n for i, card in ipairs(self.getObjects()) do\n local localCardName = card.name\n\n if i \u003c= hiddenCards then\n localCardName = hiddenCardLabel\n end\n\n cardsList = cardsList .. localCardName .. '\\n'\n end\n\n self.clearButtons()\n\n self.createButton({\n label = 'Market Deck:',\n click_function = \"none\",\n function_owner = self,\n position = {0,-0.1,-1.6},\n height = 0,\n width = 0,\n font_size = 150,\n font_color = {1,1,1}\n })\n\n self.createButton({\n label = cardsList,\n click_function = \"none\",\n function_owner = self,\n position = {0,-0.1,0.15},\n height = 0,\n width = 0,\n font_size = 115,\n font_color = {1,1,1}\n })\n\n self.createButton({\n click_function = 'revealFirstTwoCards',\n function_owner = self,\n label = 'Reveal',\n position = {-0.85,0,1.6},\n width = 375,\n height = 175,\n font_size = 90\n })\n\n self.createButton({\n click_function = 'swap',\n function_owner = self,\n label = 'Swap',\n position = {0,0,1.6},\n width = 375,\n height = 175,\n font_size = 90\n })\n\n self.createButton({\n click_function = 'finish',\n function_owner = self,\n label = 'Finish',\n position = {0.85,0,1.6},\n width = 375,\n height = 175,\n font_size = 90\n })\nend\n\nfunction takeDeckOut(guid, pos)\n local deck = self.takeObject({ guid = guid, position = pos, smooth = false })\n\n for i = 1, #deck.getObjects() do\n self.putObject(deck.takeObject({ position = pos + Vector(0, 0.1 * i, 0), smooth = false }))\n end\n\n self.shuffle()\nend\n\nfunction getRevealedCards()\n local revealedCards = {}\n\n for _, pos in ipairs(revealCardPositions) do\n for _, obj in ipairs(searchLib.atPosition(self.positionToWorld(pos), \"isCard\")) do\n table.insert(revealedCards, obj.getGUID())\n end\n end\n\n return revealedCards\nend\n\nfunction revealFirstTwoCards()\n if movingCards or #getRevealedCards() \u003e 0 then return end\n\n for i, card in ipairs(self.getObjects()) do\n movingCards = true\n\n self.takeObject({\n index = 0,\n rotation = self.getRotation(),\n position = self.positionToWorld(revealCardPositions[i]),\n callback_function = function(obj)\n obj.resting = true\n movingCards = false\n end\n })\n\n hiddenCards = hiddenCards - 1\n\n if i == 2 or #self.getObjects() == 0 then\n break\n end\n end\n\n refreshButtons()\nend\n\nfunction swap()\n if movingCards then return end\n\n local revealedCards = getRevealedCards()\n\n if #revealedCards == 2 then\n for i, revealedCardGUID in ipairs(revealedCards) do\n local revealedCard = getObjectFromGUID(revealedCardGUID)\n\n revealedCard.setPositionSmooth(self.positionToWorld(revealCardPositionsSwap[i]), false, false)\n end\n end\nend\n\nfunction finish()\n if movingCards then return end\n\n local revealedCards = getRevealedCards()\n\n movingCards = true\n\n for i, revealedCardGUID in ipairs(revealedCards) do\n self.putObject(getObjectFromGUID(revealedCardGUID))\n end\n\n Wait.time(\n function()\n movingCards = false\n end,\n 0.75)\nend\n\nfunction resetHelper()\n for i, card in ipairs(self.getObjects()) do\n self.takeObject({\n index = 0,\n smooth = false,\n rotation = self.getRotation(),\n position = self.positionToWorld(revealCardPositions[2])\n })\n end\n\n self.clearButtons()\n\n self.createButton({\n label = 'Underworld Market\\nHelper',\n click_function = \"none\",\n function_owner = self,\n position = {0,-0.1,-1.6},\n height = 0,\n width = 0,\n font_size = 145,\n font_color = {1,1,1}\n })\n\n hiddenCards = 10\n isSetup = false\n movingCards = false\n\n self.reset()\n\n print('Underworld Market Helper: Helper has been reset.')\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/UnderworldMarketHelper\")\nend)\n__bundle_register(\"accessories/UnderworldMarketHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nfunction onSave()\n return JSON.encode({ saved_hiddenCards = hiddenCards })\nend\n\nfunction onload(savedData)\n revealCardPositions = {\n Vector(3.5, 0.25, 0),\n Vector(-3.5, 0.25, 0)\n }\n\n revealCardPositionsSwap = {\n Vector(-3.5, 0.25, 0),\n Vector(3.5, 0.25, 0)\n }\n\n self.createButton({\n label = 'Underworld Market\\nHelper',\n click_function = \"none\",\n function_owner = self,\n position = { 0, -0.1, -1.6 },\n height = 0,\n width = 0,\n font_size = 145,\n font_color = { 1, 1, 1 }\n })\n\n hiddenCards = 10\n hiddenCardLabel = '-----'\n\n isSetup = false\n movingCards = false\n\n self.addContextMenuItem('Reset Helper', resetHelper)\n\n if savedData and savedData ~= '' then\n local loaded_data = JSON.decode(savedData)\n hiddenCards = loaded_data.saved_hiddenCards\n\n isSetup = true\n refreshButtons()\n end\nend\n\nfunction onObjectEnterContainer(container, object)\n if container ~= self then return end\n\n if isSetup and object.tag == \"Card\" then\n refreshButtons()\n end\n\n if object.tag == \"Deck\" then\n if validateDeck(object) then\n takeDeckOut(object.getGUID(), self.getPosition() + Vector(0, 0.1, 0))\n refreshButtons()\n\n isSetup = true\n end\n elseif object.tag ~= \"Card\" then\n broadcastToAll(\"The 'Underworld Market Helper' is meant to be used for cards.\", \"White\")\n end\nend\n\nfunction onObjectLeaveContainer(container, object)\n if container ~= self then return end\n\n if isSetup then\n refreshButtons()\n end\nend\n\nfunction validateDeck(deck)\n if deck.getQuantity() ~= 10 then\n print('Underworld Market Helper: Deck must include exactly 10 cards.')\n return false\n end\n\n local illicitCount = 0\n\n for _, card in ipairs(deck.getObjects()) do\n decodedGMNotes = JSON.decode(card.gm_notes)\n\n if decodedGMNotes ~= nil and string.find(decodedGMNotes.traits, \"Illicit\", 1, true) then\n illicitCount = illicitCount + 1\n end\n end\n\n if illicitCount ~= 10 then\n print('Underworld Market Helper: Deck must include 10 Illicit cards.')\n return false\n end\n\n return true\nend\n\nfunction refreshButtons()\n local cardsList = ''\n\n for i, card in ipairs(self.getObjects()) do\n local localCardName = card.name\n\n if i \u003c= hiddenCards then\n localCardName = hiddenCardLabel\n end\n\n cardsList = cardsList .. localCardName .. '\\n'\n end\n\n self.clearButtons()\n\n self.createButton({\n label = 'Market Deck:',\n click_function = \"none\",\n function_owner = self,\n position = { 0, -0.1, -1.6 },\n height = 0,\n width = 0,\n font_size = 150,\n font_color = { 1, 1, 1 }\n })\n\n self.createButton({\n label = cardsList,\n click_function = \"none\",\n function_owner = self,\n position = { 0, -0.1, 0.15 },\n height = 0,\n width = 0,\n font_size = 115,\n font_color = { 1, 1, 1 }\n })\n\n self.createButton({\n click_function = 'revealFirstTwoCards',\n function_owner = self,\n label = 'Reveal',\n position = { -0.85, 0, 1.6 },\n width = 375,\n height = 175,\n font_size = 90\n })\n\n self.createButton({\n click_function = 'swap',\n function_owner = self,\n label = 'Swap',\n position = { 0, 0, 1.6 },\n width = 375,\n height = 175,\n font_size = 90\n })\n\n self.createButton({\n click_function = 'finish',\n function_owner = self,\n label = 'Finish',\n position = { 0.85, 0, 1.6 },\n width = 375,\n height = 175,\n font_size = 90\n })\nend\n\nfunction takeDeckOut(guid, pos)\n local deck = self.takeObject({ guid = guid, position = pos, smooth = false })\n\n for i = 1, #deck.getObjects() do\n self.putObject(deck.takeObject({ position = pos + Vector(0, 0.1 * i, 0), smooth = false }))\n end\n\n self.shuffle()\nend\n\nfunction getRevealedCards()\n local revealedCards = {}\n\n for _, pos in ipairs(revealCardPositions) do\n for _, obj in ipairs(searchLib.atPosition(self.positionToWorld(pos), \"isCard\")) do\n table.insert(revealedCards, obj.getGUID())\n end\n end\n\n return revealedCards\nend\n\nfunction revealFirstTwoCards()\n if movingCards or #getRevealedCards() \u003e 0 then return end\n\n for i, card in ipairs(self.getObjects()) do\n movingCards = true\n\n self.takeObject({\n index = 0,\n rotation = self.getRotation(),\n position = self.positionToWorld(revealCardPositions[i]),\n callback_function = function(obj)\n obj.resting = true\n movingCards = false\n end\n })\n\n hiddenCards = hiddenCards - 1\n\n if i == 2 or #self.getObjects() == 0 then\n break\n end\n end\n\n refreshButtons()\nend\n\nfunction swap()\n if movingCards then return end\n\n local revealedCards = getRevealedCards()\n\n if #revealedCards == 2 then\n for i, revealedCardGUID in ipairs(revealedCards) do\n local revealedCard = getObjectFromGUID(revealedCardGUID)\n\n revealedCard.setPositionSmooth(self.positionToWorld(revealCardPositionsSwap[i]), false, false)\n end\n end\nend\n\nfunction finish()\n if movingCards then return end\n\n local revealedCards = getRevealedCards()\n\n movingCards = true\n\n for i, revealedCardGUID in ipairs(revealedCards) do\n self.putObject(getObjectFromGUID(revealedCardGUID))\n end\n\n Wait.time(\n function()\n movingCards = false\n end,\n 0.75)\nend\n\nfunction resetHelper(playerColor)\n Player[playerColor].clearSelectedObjects()\n for i, card in ipairs(self.getObjects()) do\n self.takeObject({\n index = 0,\n smooth = false,\n rotation = self.getRotation(),\n position = self.positionToWorld(revealCardPositions[2])\n })\n end\n\n self.clearButtons()\n\n self.createButton({\n label = 'Underworld Market\\nHelper',\n click_function = \"none\",\n function_owner = self,\n position = { 0, -0.1, -1.6 },\n height = 0,\n width = 0,\n font_size = 145,\n font_color = { 1, 1, 1 }\n })\n\n hiddenCards = 10\n isSetup = false\n movingCards = false\n\n print('Underworld Market Helper: Helper has been reset.')\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MaterialIndex": -1, "MeasureMovement": false, @@ -47842,7 +28155,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"accessories/Subject5U-21Helper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal classOrder = {\n \"Guardian\",\n \"Seeker\",\n \"Survivor\",\n \"Mystic\",\n \"Rogue\"\n}\n\nlocal bParam = {}\nbParam.width = 0\nbParam.height = 0\nbParam.function_owner = self\nbParam.click_function = \"none\"\nbParam.label = \"0\"\nbParam.position = {x = 0, y = 0.1, z = -0.7}\nbParam.scale = {x = 0.1, y = 0.1, z = 0.1}\nbParam.font_color = \"White\"\nbParam.font_size = 700\n\nfunction onLoad()\n self.createButton({\n width = 2750,\n height = 800,\n function_owner = self,\n click_function = \"updateDisplayButtons\",\n label = \"Update!\",\n tooltip = \"Count classes from cards on this tile\",\n position = {x = 0, y = 0.1, z = 0.875},\n scale = {x = 0.1, y = 0.1, z = 0.1},\n font_size = 500\n })\n createDisplayButtons()\nend\n\nfunction createDisplayButtons()\n local x_offset = 0.361\n bParam.position.x = -3 * x_offset\n for i = 1, 5 do\n bParam.position.x = bParam.position.x + x_offset\n self.createButton(bParam)\n end\nend\n\nfunction updateDisplayButtons(_, playerColor)\n local classCount = {\n Guardian = 0,\n Seeker = 0,\n Survivor = 0,\n Mystic = 0,\n Rogue = 0,\n uncounted = 0\n }\n\n -- loop through cards on this helper and count classes from metadata\n for _, notes in ipairs(getNotesFromCardsAndContainers()) do\n if notes.class then\n for str in string.gmatch(notes.class, \"([^|]+)\") do\n if not tonumber(classCount[str]) then\n str = \"uncounted\"\n end\n classCount[str] = classCount[str] + 1\n end\n end\n end\n\n -- edit button labels with index 1-5\n for i = 1, 5 do\n self.editButton({index = i, label = classCount[classOrder[i]]})\n end\n \n -- show message about uncounted cards\n if classCount.uncounted \u003e 0 then\n printToColor(\"Search included \" .. classCount.uncounted .. \" neutral/ununcounted card(s).\", playerColor, \"Orange\")\n end\nend\n\nfunction getNotesFromCardsAndContainers()\n local notesList = {}\n for _, obj in ipairs(searchLib.onObject(self)) do\n local notes = {}\n if obj.type == \"Card\" then\n notes = JSON.decode(obj.getGMNotes()) or {}\n table.insert(notesList, notes)\n elseif obj.type == \"Bag\" or obj.type == \"Deck\" then\n -- check if there are actually objects contained and loop through them\n local containedObjects = obj.getData().ContainedObjects\n if containedObjects then\n for _, deepObj in ipairs(containedObjects) do\n if deepObj.Name == \"Card\" or deepObj.Name == \"CardCustom\" then\n notes = JSON.decode(deepObj.GMNotes) or {}\n table.insert(notesList, notes)\n end\n end\n end\n end\n end\n return notesList\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/Subject5U-21Helper\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/Subject5U-21Helper\")\nend)\n__bundle_register(\"accessories/Subject5U-21Helper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal classOrder = {\n \"Guardian\",\n \"Seeker\",\n \"Survivor\",\n \"Mystic\",\n \"Rogue\"\n}\n\nlocal bParam = {}\nbParam.width = 0\nbParam.height = 0\nbParam.function_owner = self\nbParam.click_function = \"none\"\nbParam.label = \"0\"\nbParam.position = {x = 0, y = 0.1, z = -0.7}\nbParam.scale = {x = 0.1, y = 0.1, z = 0.1}\nbParam.font_color = \"White\"\nbParam.font_size = 700\n\nfunction onLoad()\n self.createButton({\n width = 2750,\n height = 800,\n function_owner = self,\n click_function = \"updateDisplayButtons\",\n label = \"Update!\",\n tooltip = \"Count classes from cards on this tile\",\n position = {x = 0, y = 0.1, z = 0.875},\n scale = {x = 0.1, y = 0.1, z = 0.1},\n font_size = 500\n })\n createDisplayButtons()\nend\n\nfunction createDisplayButtons()\n local x_offset = 0.361\n bParam.position.x = -3 * x_offset\n for i = 1, 5 do\n bParam.position.x = bParam.position.x + x_offset\n self.createButton(bParam)\n end\nend\n\nfunction updateDisplayButtons(_, playerColor)\n local classCount = {\n Guardian = 0,\n Seeker = 0,\n Survivor = 0,\n Mystic = 0,\n Rogue = 0,\n uncounted = 0\n }\n\n -- loop through cards on this helper and count classes from metadata\n for _, notes in ipairs(getNotesFromCardsAndContainers()) do\n if notes.class then\n for str in string.gmatch(notes.class, \"([^|]+)\") do\n if not tonumber(classCount[str]) then\n str = \"uncounted\"\n end\n classCount[str] = classCount[str] + 1\n end\n end\n end\n\n -- edit button labels with index 1-5\n for i = 1, 5 do\n self.editButton({index = i, label = classCount[classOrder[i]]})\n end\n \n -- show message about uncounted cards\n if classCount.uncounted \u003e 0 then\n printToColor(\"Search included \" .. classCount.uncounted .. \" neutral/ununcounted card(s).\", playerColor, \"Orange\")\n end\nend\n\nfunction getNotesFromCardsAndContainers()\n local notesList = {}\n for _, obj in ipairs(searchLib.onObject(self)) do\n local notes = {}\n if obj.type == \"Card\" then\n notes = JSON.decode(obj.getGMNotes()) or {}\n table.insert(notesList, notes)\n elseif obj.type == \"Bag\" or obj.type == \"Deck\" then\n -- check if there are actually objects contained and loop through them\n local containedObjects = obj.getData().ContainedObjects\n if containedObjects then\n for _, deepObj in ipairs(containedObjects) do\n if deepObj.Name == \"Card\" or deepObj.Name == \"CardCustom\" then\n notes = JSON.decode(deepObj.GMNotes) or {}\n table.insert(notesList, notes)\n end\n end\n end\n end\n end\n return notesList\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Tile", @@ -48142,7 +28455,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"ml\":[]}", "MeasureMovement": false, "Name": "Custom_Model", @@ -48211,7 +28524,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48280,7 +28593,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48320,7 +28633,7 @@ }, "CustomMesh": { "CastShadows": true, - "ColliderURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_COL.obj", + "ColliderURL": "", "Convex": true, "CustomShader": { "FresnelStrength": 0, @@ -48349,7 +28662,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48362,7 +28675,7 @@ "Tooltip": true, "Transform": { "posX": 50.2, - "posY": 1.486, + "posY": 1.481, "posZ": -78, "rotX": 0, "rotY": 270, @@ -48389,7 +28702,7 @@ }, "CustomMesh": { "CastShadows": true, - "ColliderURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_COL.obj", + "ColliderURL": "", "Convex": true, "CustomShader": { "FresnelStrength": 0, @@ -48418,7 +28731,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48431,7 +28744,7 @@ "Tooltip": true, "Transform": { "posX": 35.4, - "posY": 1.486, + "posY": 1.481, "posZ": -52, "rotX": 0, "rotY": 270, @@ -48458,7 +28771,7 @@ }, "CustomMesh": { "CastShadows": true, - "ColliderURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_COL.obj", + "ColliderURL": "", "Convex": true, "CustomShader": { "FresnelStrength": 0, @@ -48487,7 +28800,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48500,7 +28813,7 @@ "Tooltip": true, "Transform": { "posX": 20.6, - "posY": 1.486, + "posY": 1.481, "posZ": -52, "rotX": 0, "rotY": 270, @@ -48520,9 +28833,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "a": 0.27451, + "a": 0.27, "b": 1, - "g": 0.99608, + "g": 1, "r": 1 }, "CustomMesh": { @@ -48556,7 +28869,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48589,9 +28902,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "a": 0.27451, + "a": 0.27, "b": 1, - "g": 0.99608, + "g": 1, "r": 1 }, "CustomMesh": { @@ -48625,7 +28938,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48658,9 +28971,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "a": 0.27059, + "a": 0.27, "b": 1, - "g": 0.99608, + "g": 1, "r": 1 }, "CustomMesh": { @@ -48694,7 +29007,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -48727,9 +29040,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "a": 0.27451, + "a": 0.27, "b": 1, - "g": 0.99608, + "g": 1, "r": 1 }, "CustomMesh": { @@ -48763,7 +29076,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -49810,7 +30123,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DataHelper\")\nend)\n__bundle_register(\"core/DataHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- data for difficulty selector scripts to set up chaos bag\nmodeData = {\n ['Core Set'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Devourer Below'] = {\n easy = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' },\n normal = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' },\n hard = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' },\n expert = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' }\n },\n\n -----------------The Dunwich Legacy\n ['The Dunwich Legacy'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'red', 'blue' } }\n },\n ['The Miskatonic Museum'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Essex County Express'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Blood on the Altar'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Undimensioned and Unseen'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Where Doom Awaits'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Lost in Time and Space'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Path to Carcosa\n ['The Path to Carcosa'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'skull', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'red', 'blue' } }\n },\n ['The Last King'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['Echoes of the Past'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['The Unspeakable Oath'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['A Phantom of Truth'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['The Pallid Mask'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['Black Stars Rise'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['Dim Carcosa'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'red', 'blue' } }\n },\n\n -----------------The Forgotten Age\n ['The Forgotten Age'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['The Doom of Eztli'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Threads of Fate'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Boundary Beyond'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The City of Archives'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Depths of Yoth'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Heart of the Elders'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Shattered Aeons'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Circle Undone\n ['The Circle Undone'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm6', 'm8', 'skull', 'skull', 'red', 'blue' } }\n },\n [\"At Death's Doorstep\"] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Secret Name'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Wages of Sin'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['For the Greater Good'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Union and Disillusion'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['In the Clutches of Chaos'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Before the Black Throne'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Dream-Eaters\n ['TDE_A'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['TDE_B'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n ['The Search For Kadath'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['A Thousand Shapes of Horror'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Dark Side of the Moon'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Point of No Return'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Where the Gods Dwell'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Weaver of the Cosmos'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Innsmouth Conspiracy\n ['The Innsmouth Conspiracy'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } } ,\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['TIC_Standalone'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n\n -----------------Edge of the Earth\n ['Edge of the Earth'] = {\n easy = { token = { 'p1', 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm7', 'frost', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['City of the Elder Things'] = {\n easy = { token = { 'p1', 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm7', 'frost', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Scarlet Keys\n ['The Scarlet Keys'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Side Missions\n --official\n ['Curse of the Rougarou'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Carnevale of Horrors'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Labyrinths of Lunacy'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { 'p1', '0','m1', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'red', 'blue' } }\n },\n ['Guardians of the Abyss'] = {\n normal = { token = { 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Excelsior'] = {\n normal = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Read or Die'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['All or Nothing'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Meowlathotep'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['WotOG'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'skull', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'skull', 'red', 'blue' } }\n },\n ['Bad Blood'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Machinations'] = {\n easy = { token = { 'p1', 'p1', 'p1', '0', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Red Tide'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['FaF'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm6', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n\n --fan-made\n ['Carnevale of Spiders'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Nephew Calls'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Outsider'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Stranger Things'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Winter Winds'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'skull', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'cultist', 'red', 'blue' } }\n },\n ['The Festival'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Forbidding Desert'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Happys Funhouse'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm5', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Knightfall'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'm8', 'cultist', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Last Call at Roxies'] = {\n easy = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['The Limens of Belief'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Blood Spilled in Salem'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Bread and Circuses'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['Bridge of Sighs'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Collector'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Colour out of Space'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Curse of Amultep'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Dying Star'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'blue', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'blue', 'red', 'blue' } }\n },\n ['Against the Wendigo'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Pensher Wyrm'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Approaching Storm'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Into the Shadowlands'] = {\n easy = { token = { 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['London Set 1'] = {\n easy = { token = { 'p2', 'p1', '0', '0', '0', 'm1', 'm2', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm2', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n },\n ['London Set 2'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'elder', 'tablet', 'red', 'blue' } },\n },\n ['London Set 3'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n },\n ['Delta Green'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Jennys Choice'] = {\n easy = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4','skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm5', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Blob'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['The Initiation'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['Consternation'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'red', 'blue' } },\n },\n ['Of Sphinx'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'elder', 'cultist', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'elder', 'cultist', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'elder', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['Ordis'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['Darkness Falls'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['War of the Worlds'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['Alice in Wonderland'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['Pokemon'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'm8', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Safari'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Cerulean'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Erich Zann'] = {\n easy = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Kaimonogatari'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm6', 'm8', 'skull', 'skull', 'cultist', 'red', 'blue' } }\n },\n ['Sleepy Hollow'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Flesh'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n },\n ['Dark Matter'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } }\n },\n ['Dont Starve'] = {\n normal = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm5', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['XXXX'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n }\n}\n\nfunction onSave()\n local globalState = JSON.encode(SPAWNED_PLAYER_CARD_GUIDS)\n return globalState\nend\n\nfunction onLoad(save_state)\n if save_state ~= '' then\n SPAWNED_PLAYER_CARD_GUIDS = JSON.decode(save_state)\n else\n SPAWNED_PLAYER_CARD_GUIDS = {}\n end\nend\n\nfunction getSpawnedPlayerCardGuid(params)\n local guid = params[1]\n if SPAWNED_PLAYER_CARD_GUIDS == nil then\n return nil\n else\n return SPAWNED_PLAYER_CARD_GUIDS[guid]\n end\nend\n\nfunction setSpawnedPlayerCardGuid(params)\n local guid = params[1]\n local value = params[2]\n if SPAWNED_PLAYER_CARD_GUIDS ~= nil then\n SPAWNED_PLAYER_CARD_GUIDS[guid] = value\n return true\n else\n return false\n end\nend\n\n-- called by \"Global\" during encounter card drawing\nfunction checkHiddenCard(name)\n for _, n in ipairs(HIDDEN_CARD_DATA) do\n if name == n then\n return true\n end\n end\n return false\nend\n\n-- called by custom data helpers\nfunction updateHiddenCards(args)\n local custom_data_helper = getObjectFromGUID(args[1])\n local data_hiddenCards = custom_data_helper.getTable(\"HIDDEN_CARD_DATA\")\n for k, v in ipairs(data_hiddenCards) do\n table.insert(HIDDEN_CARD_DATA, v)\n end\nend\n\n--------------------------------------------------------------------------\n-- deprecated code, kept here for existing calls\n--------------------------------------------------------------------------\n\n-- deprecated, use metadata (GM Notes) instead\n-- Known locations and clues. We check this to determine if we should atttempt to spawn clues,\n-- first we look for \u003cLOCATION_NAME\u003e_\u003cGUID\u003e and if we find nothing we look for \u003cLOCATION_NAME\u003e\n-- format is [location_guid -\u003e clueCount]\n\n-- Example 1: \"Study\" from Core Set (https://arkhamdb.com/card/01111)\n-- [\"Study\"]= {type=\"perPlayer\", value=2, clueSide= \"back\"}\n\n-- Example 2: \"Student Union\" from Dunwich Legacy (https://arkhamdb.com/card/02051)\n-- [\"Student Union\"]= {type=\"fixed\", value= 2, clueSide= \"back\"}\nLOCATIONS_DATA = {}\n\n-- deprecated, use metadata (GM Notes) instead\n-- Player cards with token counts and types\n\n-- Example: \"Flashlight\" from Core Set (https://arkhamdb.com/card/01087)\n-- [\"Flashlight\"]= {tokenType= \"resource\", tokenCount= 3}\nPLAYER_CARD_DATA = {}\n\n-- deprecated, use metadata (GM Notes) instead (still used by custom data helpers)\n-- Encounter Cards with \"Hidden.\" (List of names)\n\n-- Example: \"Possession (Murderous)\" from Path to Carcosa (https://arkhamdb.com/card/03342)\n-- ..., \"Possession (Murderous)\", ...\nHIDDEN_CARD_DATA = {}\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DataHelper\")\nend)\n__bundle_register(\"core/DataHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- data for difficulty selector scripts to set up chaos bag\nmodeData = {\n ['Core Set'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Devourer Below'] = {\n easy = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' },\n normal = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' },\n hard = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' },\n expert = { parent = 'Core Set', append = { 'elder' }, message = 'An additional token for the preparation of this scenario has been added to the bag.' }\n },\n\n -----------------The Dunwich Legacy\n ['The Dunwich Legacy'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'red', 'blue' } }\n },\n ['The Miskatonic Museum'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Essex County Express'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Blood on the Altar'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Undimensioned and Unseen'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Where Doom Awaits'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Lost in Time and Space'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Path to Carcosa\n ['The Path to Carcosa'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'skull', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'red', 'blue' } }\n },\n ['The Last King'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['Echoes of the Past'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['The Unspeakable Oath'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['A Phantom of Truth'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['The Pallid Mask'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['Black Stars Rise'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'red', 'blue' }, random = { {'cultist', 'cultist'}, {'tablet', 'tablet'}, {'elder', 'elder'} } }\n },\n ['Dim Carcosa'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'red', 'blue' } }\n },\n\n -----------------The Forgotten Age\n ['The Forgotten Age'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['The Doom of Eztli'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Threads of Fate'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Boundary Beyond'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The City of Archives'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Depths of Yoth'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Heart of the Elders'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Shattered Aeons'] = {\n standalone = { token = { 'p1', '0', '0', '0','m1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Circle Undone\n ['The Circle Undone'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm6', 'm8', 'skull', 'skull', 'red', 'blue' } }\n },\n [\"At Death's Doorstep\"] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Secret Name'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Wages of Sin'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['For the Greater Good'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Union and Disillusion'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['In the Clutches of Chaos'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Before the Black Throne'] = {\n standalone = { token = { 'p1', '0', '0', 'm1','m1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Dream-Eaters\n ['TDE_A'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['TDE_B'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n ['The Search For Kadath'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['A Thousand Shapes of Horror'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Dark Side of the Moon'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Point of No Return'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Where the Gods Dwell'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Weaver of the Cosmos'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'cultist', 'elder', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Innsmouth Conspiracy\n ['The Innsmouth Conspiracy'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } } ,\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['TIC_Standalone'] = {\n standalone = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n\n -----------------Edge of the Earth\n ['Edge of the Earth'] = {\n easy = { token = { 'p1', 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm7', 'frost', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['City of the Elder Things'] = {\n easy = { token = { 'p1', 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm7', 'frost', 'frost', 'frost', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Scarlet Keys\n ['The Scarlet Keys'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n\n -----------------The Side Missions\n --official\n ['Curse of the Rougarou'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Carnevale of Horrors'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Labyrinths of Lunacy'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { 'p1', '0','m1', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'red', 'blue' } }\n },\n ['Guardians of the Abyss'] = {\n normal = { token = { 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Excelsior'] = {\n normal = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Read or Die'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['All or Nothing'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Meowlathotep'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['WotOG'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'skull', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'skull', 'red', 'blue' } }\n },\n ['Bad Blood'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Machinations'] = {\n easy = { token = { 'p1', 'p1', 'p1', '0', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Red Tide'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['FaF'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm6', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n\n --fan-made\n ['Carnevale of Spiders'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Nephew Calls'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Outsider'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Stranger Things'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Winter Winds'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'skull', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'cultist', 'red', 'blue' } }\n },\n ['The Festival'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Forbidding Desert'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Happys Funhouse'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm5', 'm7', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Knightfall'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'm8', 'cultist', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Last Call at Roxies'] = {\n easy = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['The Limens of Belief'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } }\n },\n ['Blood Spilled in Salem'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Bread and Circuses'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['Bridge of Sighs'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Collector'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Colour out of Space'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Curse of Amultep'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['The Dying Star'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'blue', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'blue', 'red', 'blue' } }\n },\n ['Against the Wendigo'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Pensher Wyrm'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'elder', 'red', 'blue' } }\n },\n ['Approaching Storm'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Into the Shadowlands'] = {\n easy = { token = { 'p1', 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['London Set 1'] = {\n easy = { token = { 'p2', 'p1', '0', '0', '0', 'm1', 'm2', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm2', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n },\n ['London Set 2'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'elder', 'tablet', 'red', 'blue' } },\n },\n ['London Set 3'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n },\n ['Delta Green'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Jennys Choice'] = {\n easy = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4','skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm5', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['The Blob'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['The Initiation'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['Consternation'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'm7', 'skull', 'skull', 'skull', 'red', 'blue' } },\n },\n ['Of Sphinx'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'elder', 'cultist', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'skull', 'elder', 'cultist', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'elder', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['Ordis'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'elder', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['Darkness Falls'] = {\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n },\n ['War of the Worlds'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['Alice in Wonderland'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'm5', 'm6', 'skull', 'skull', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'skull', 'skull', 'elder', 'red', 'blue' } }\n },\n ['Pokemon'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'm8', 'skull', 'skull', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Safari'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Cerulean'] = {\n normal = { token = { 'p1', '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', 'm1', 'm2', 'm3', 'm3', 'm4', 'm6', 'skull', 'skull', 'cultist', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Erich Zann'] = {\n easy = { token = { 'p1', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n normal = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } }\n },\n ['Kaimonogatari'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm2', 'm2', 'm3', 'm4', 'm4', 'm5', 'skull', 'skull', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', '0', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm6', 'm8', 'skull', 'skull', 'cultist', 'red', 'blue' } }\n },\n ['Sleepy Hollow'] = {\n normal = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['Flesh'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm3', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', 'm1', 'm1', 'm2', 'm3', 'm3', 'm4', 'm4', 'm6', 'skull', 'skull', 'cultist', 'tablet', 'tablet', 'red', 'blue' } },\n },\n ['Dark Matter'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'cultist', 'red', 'blue' } }\n },\n ['Dont Starve'] = {\n normal = { token = { 'p1', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n hard = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm5', 'm7', 'skull', 'skull', 'cultist', 'tablet', 'elder', 'red', 'blue' } },\n },\n ['XXXX'] = {\n easy = { token = { 'p1', 'p1', '0', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n normal = { token = { 'p1', '0', '0', 'm1', 'm1', 'm1', 'm2', 'm2', 'm3', 'm4', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n hard = { token = { '0', '0', '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm5', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } },\n expert = { token = { '0', 'm1', 'm1', 'm2', 'm2', 'm3', 'm3', 'm4', 'm4', 'm5', 'm6', 'm8', 'skull', 'skull', 'cultist', 'tablet', 'red', 'blue' } }\n }\n}\n\nfunction onSave()\n return JSON.encode(SPAWNED_PLAYER_CARD_GUIDS)\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= '' then\n SPAWNED_PLAYER_CARD_GUIDS = JSON.decode(savedData)\n else\n SPAWNED_PLAYER_CARD_GUIDS = {}\n end\nend\n\nfunction getSpawnedPlayerCardGuid(params)\n local guid = params[1]\n if SPAWNED_PLAYER_CARD_GUIDS == nil then\n return nil\n else\n return SPAWNED_PLAYER_CARD_GUIDS[guid]\n end\nend\n\nfunction setSpawnedPlayerCardGuid(params)\n local guid = params[1]\n local value = params[2]\n if SPAWNED_PLAYER_CARD_GUIDS ~= nil then\n SPAWNED_PLAYER_CARD_GUIDS[guid] = value\n return true\n else\n return false\n end\nend\n\n-- called by \"Global\" during encounter card drawing\nfunction checkHiddenCard(name)\n for _, n in ipairs(HIDDEN_CARD_DATA) do\n if name == n then\n return true\n end\n end\n return false\nend\n\n-- called by custom data helpers\nfunction updateHiddenCards(args)\n local custom_data_helper = getObjectFromGUID(args[1])\n local data_hiddenCards = custom_data_helper.getTable(\"HIDDEN_CARD_DATA\")\n for k, v in ipairs(data_hiddenCards) do\n table.insert(HIDDEN_CARD_DATA, v)\n end\nend\n\n--------------------------------------------------------------------------\n-- deprecated code, kept here for existing calls\n--------------------------------------------------------------------------\n\n-- deprecated, use metadata (GM Notes) instead\n-- Known locations and clues. We check this to determine if we should atttempt to spawn clues,\n-- first we look for \u003cLOCATION_NAME\u003e_\u003cGUID\u003e and if we find nothing we look for \u003cLOCATION_NAME\u003e\n-- format is [location_guid -\u003e clueCount]\n\n-- Example 1: \"Study\" from Core Set (https://arkhamdb.com/card/01111)\n-- [\"Study\"]= {type=\"perPlayer\", value=2, clueSide= \"back\"}\n\n-- Example 2: \"Student Union\" from Dunwich Legacy (https://arkhamdb.com/card/02051)\n-- [\"Student Union\"]= {type=\"fixed\", value= 2, clueSide= \"back\"}\nLOCATIONS_DATA = {}\n\n-- deprecated, use metadata (GM Notes) instead\n-- Player cards with token counts and types\n\n-- Example: \"Flashlight\" from Core Set (https://arkhamdb.com/card/01087)\n-- [\"Flashlight\"]= {tokenType= \"resource\", tokenCount= 3}\nPLAYER_CARD_DATA = {}\n\n-- deprecated, use metadata (GM Notes) instead (still used by custom data helpers)\n-- Encounter Cards with \"Hidden.\" (List of names)\n\n-- Example: \"Possession (Murderous)\" from Path to Carcosa (https://arkhamdb.com/card/03342)\n-- ..., \"Possession (Murderous)\", ...\nHIDDEN_CARD_DATA = {}\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[]", "MeasureMovement": false, "Name": "Custom_Tile", @@ -49867,7 +30180,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/BlessCurseManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\n-- common button parameters\nlocal buttonParamaters = {}\nbuttonParamaters.function_owner = self\nbuttonParamaters.color = { 0, 0, 0, 0 }\nbuttonParamaters.width = 700\nbuttonParamaters.height = 700\n\nlocal updating\n\n---------------------------------------------------------\n-- creating buttons and menus + initializing tables\n---------------------------------------------------------\n\nfunction onLoad()\n -- index: 0 - bless\n buttonParamaters.click_function = \"clickBless\"\n buttonParamaters.position = { -1.03, 0, 0.46 }\n buttonParamaters.tooltip = \"Add / Remove Bless\"\n self.createButton(buttonParamaters)\n\n -- index: 1 - curse\n buttonParamaters.click_function = \"clickCurse\"\n buttonParamaters.position[1] = -buttonParamaters.position[1]\n buttonParamaters.tooltip = \"Add / Remove Curse\"\n self.createButton(buttonParamaters)\n\n -- index: 2 - bless count\n buttonParamaters.tooltip = \"\"\n buttonParamaters.click_function = \"none\"\n buttonParamaters.width = 0\n buttonParamaters.height = 0\n buttonParamaters.color = { 0.4, 0.4, 0.4 }\n buttonParamaters.font_color = { 1, 1, 1 }\n buttonParamaters.font_size = 235\n buttonParamaters.position = { -1.03, 0.06, -0.8 }\n self.createButton(buttonParamaters)\n\n -- index: 3 - curse count\n buttonParamaters.position[1] = -buttonParamaters.position[1]\n self.createButton(buttonParamaters)\n\n -- context menu\n self.addContextMenuItem(\"Remove all\", doRemove)\n self.addContextMenuItem(\"Reset\", doReset)\n\n -- initializing tables\n initializeState()\n broadcastCount(\"Curse\")\n broadcastCount(\"Bless\")\nend\n\nfunction resetTables()\n numInPlay = { Bless = 0, Curse = 0 }\n tokensTaken = { Bless = {}, Curse = {} }\n sealedTokens = {}\nend\n\nfunction initializeState()\n resetTables()\n\n -- count tokens in the bag\n local chaosBag = chaosBagApi.findChaosBag()\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == \"Bless\" then\n numInPlay.Bless = numInPlay.Bless + 1\n elseif v.name == \"Curse\" then\n numInPlay.Curse = numInPlay.Curse + 1\n end\n end\n\n -- find tokens in the play area\n for _, obj in ipairs(getObjects()) do\n local pos = obj.getPosition()\n if obj.hasTag(\"tempToken\") then\n -- skip the tokens from the Token Arranger\n elseif pos.x \u003e -65 and pos.x \u003c 10 and pos.z \u003e -35 and pos.z \u003c 35 and obj.type == \"Tile\" then\n if obj.getName() == \"Bless\" then\n table.insert(tokensTaken.Bless, obj.getGUID())\n numInPlay.Bless = numInPlay.Bless + 1\n elseif obj.getName() == \"Curse\" then\n table.insert(tokensTaken.Curse, obj.getGUID())\n numInPlay.Curse = numInPlay.Curse + 1\n end\n end\n end\n\n updateButtonLabels()\nend\n\nfunction updateButtonLabels()\n self.editButton({ index = 2, label = formatTokenCount(\"Bless\", true)})\n self.editButton({ index = 3, label = formatTokenCount(\"Curse\", true)})\nend\n\nfunction broadcastCount(token)\n local count = formatTokenCount(token)\n if count == \"(0 + 0)\" then return end\n broadcastToAll(token .. \" Tokens \" .. count, \"White\")\nend\n\nfunction broadcastStatus(color)\n broadcastToColor(\"Curse Tokens \" .. formatTokenCount(\"Curse\"), color, \"White\")\n broadcastToColor(\"Bless Tokens \" .. formatTokenCount(\"Bless\"), color, \"White\")\nend\n\n---------------------------------------------------------\n-- TTS event handling\n---------------------------------------------------------\n\n-- enable tracking of bag changes for 1 second\nfunction onObjectDrop(_, object)\n if not isBlurseToken(object) then return end\n\n -- check if object was dropped in chaos bag area\n for _, zone in ipairs(object.getZones()) do\n if zone.getName() == \"ChaosBagZone\" then\n trackBagChange = true\n Wait.time(function() trackBagChange = false end, 1)\n return\n end\n end\nend\n\n-- handle manual returning of bless / curse tokens\nfunction onObjectEnterContainer(container, object)\n if not (trackBagChange and isChaosbag(container) and isBlurseToken(object)) then return end\n\n -- small delay to ensure token has entered bag\n Wait.time(doReset, 0.5)\nend\n\nfunction isChaosbag(obj)\n if obj.getDescription() ~= \"Chaos Bag\" then return end -- early exit for performance\n return obj == chaosBagApi.findChaosBag()\nend\n\nfunction isBlurseToken(obj)\n local name = obj.getName()\n return name == \"Bless\" or name == \"Curse\"\nend\n\n---------------------------------------------------------\n-- context menu functions\n---------------------------------------------------------\n\nfunction doRemove(color)\n local chaosBag = chaosBagApi.findChaosBag()\n\n -- remove tokens from chaos bag\n local count = { Bless = 0, Curse = 0 }\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == \"Bless\" or v.name == \"Curse\" then\n chaosBag.takeObject({\n guid = v.guid,\n position = { 0, 5, 0 },\n callback_function = function(obj) obj.destruct() end\n })\n count[v.name] = count[v.name] + 1\n end\n end\n\n broadcastToColor(\"Removed \" .. count.Bless .. \" Bless and \" .. count.Curse .. \" Curse tokens from the chaos bag.\", color, \"White\")\n broadcastToColor(\"Removed \" .. removeTakenTokens(\"Bless\") .. \" Bless and \" .. removeTakenTokens(\"Curse\") .. \" Curse tokens from play.\", color, \"White\")\n\n resetTables()\n updateButtonLabels()\n tokenArrangerApi.layout()\nend\n\nfunction doReset()\n initializeState()\n broadcastCount(\"Curse\")\n broadcastCount(\"Bless\")\n tokenArrangerApi.layout()\nend\n\n---------------------------------------------------------\n-- click functions\n---------------------------------------------------------\n\nfunction clickBless(_, color, isRightClick)\n playerColor = color\n callFunctions(\"Bless\", isRightClick)\nend\n\nfunction clickCurse(_, color, isRightClick)\n playerColor = color\n callFunctions(\"Curse\", isRightClick)\nend\n\nfunction callFunctions(type, isRightClick)\n if not chaosBagApi.canTouchChaosTokens() then return end\n\n if isRightClick then\n removeToken(type)\n else\n addToken(type)\n end\n\n tokenArrangerApi.layout()\nend\n\n---------------------------------------------------------\n-- called functions\n---------------------------------------------------------\n\n-- returns a formatted string with information about the provided token type (bless / curse)\n---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n---@param omitBrackets? boolean Controls whether the brackets should be omitted from the return\n---@return string tokenCount\nfunction formatTokenCount(type, omitBrackets)\n if omitBrackets then\n return (numInPlay[type] - #tokensTaken[type]) .. \" + \" .. #tokensTaken[type]\n else\n return \"(\" .. (numInPlay[type] - #tokensTaken[type]) .. \" + \" .. #tokensTaken[type] .. \")\"\n end\nend\n\n-- seals a token on a card (called by cards that seal bless/curse tokens)\n---@param param table This contains the type and guid of the sealed token\nfunction sealedToken(param)\n table.insert(tokensTaken[param.type], param.guid)\n broadcastCount(param.type)\n updateButtonLabels()\nend\n\n-- returns a token to the bag (called by cards that seal bless/curse tokens)\n---@param param table This contains the type and guid of the released token\nfunction releasedToken(param)\n for i, v in ipairs(tokensTaken[param.type]) do\n if v == param.guid then\n table.remove(tokensTaken[param.type], i)\n break\n end\n end\n updateDisplayAndBroadcast(param.type)\nend\n\n-- removes a token (called by cards that seal bless/curse tokens)\n---@param param table This contains the type and guid of the released token\nfunction returnedToken(param)\n for i, v in ipairs(tokensTaken[param.type]) do\n if v == param.guid then\n table.remove(tokensTaken[param.type], i)\n numInPlay[param.type] = numInPlay[param.type] - 1\n break\n end\n end\n updateDisplayAndBroadcast(param.type)\nend\n\nfunction updateDisplayAndBroadcast(type)\n if not updating then\n updating = true\n Wait.frames(function()\n broadcastCount(type)\n updateButtonLabels()\n updating = false\n end, 5)\n end\nend\n\n---------------------------------------------------------\n-- main functions: add and remove\n---------------------------------------------------------\n\nfunction addToken(type)\n if numInPlay[type] == 10 then\n printToColor(\"10 tokens already in play, not adding any.\", playerColor)\n return\n end\n numInPlay[type] = numInPlay[type] + 1\n printToAll(\"Adding \" .. type .. \" token \" .. formatTokenCount(type))\n updateButtonLabels()\n return chaosBagApi.spawnChaosToken(type)\nend\n\nfunction removeToken(type)\n local chaosBag = chaosBagApi.findChaosBag()\n local tokens = {}\n\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == type then\n table.insert(tokens, v.guid)\n end\n end\n\n if #tokens == 0 then\n printToColor(\"No \" .. type .. \" tokens in the chaos bag.\", playerColor)\n return\n end\n\n chaosBag.takeObject({\n guid = table.remove(tokens),\n smooth = false,\n callback_function = function(obj)\n numInPlay[type] = numInPlay[type] - 1\n printToAll(\"Removing \" .. type .. \" token \" .. formatTokenCount(type))\n updateButtonLabels()\n obj.destruct()\n end\n })\nend\n\n-- removing tokens that were 'taken'\nfunction removeTakenTokens(type)\n local count = 0\n for _, guid in ipairs(tokensTaken[type]) do\n local token = getObjectFromGUID(guid)\n if token ~= nil then\n token.destruct()\n count = count + 1\n end\n end\n return count\nend\n\n---------------------------------------------------------\n-- Wendy's Menu (context menu for cards on hotkey press)\n---------------------------------------------------------\n\nfunction addMenuOptions(parameters)\n local playerColor = parameters.playerColor\n local hoveredObject = parameters.hoveredObject\n if hoveredObject == nil or hoveredObject.type ~= \"Card\" then\n broadcastToColor(\"Right-click seal options can only be added to cards.\", playerColor)\n return\n elseif hoveredObject.hasTag(\"CardThatSeals\") or hoveredObject.getVar(\"MENU_ADDED\") == true then\n broadcastToColor(\"This card already has a sealing context menu.\", playerColor)\n return\n end\n\n hoveredObject.addContextMenuItem(\"Seal Bless\", function(color)\n sealToken(\"Bless\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n hoveredObject.addContextMenuItem(\"Release Bless\", function(color)\n releaseToken(\"Bless\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n hoveredObject.addContextMenuItem(\"Seal Curse\", function(color)\n sealToken(\"Curse\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n hoveredObject.addContextMenuItem(\"Release Curse\", function(color)\n releaseToken(\"Curse\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n broadcastToColor(\"Right-click seal options added to \" .. hoveredObject.getName(), playerColor)\n hoveredObject.setVar(\"MENU_ADDED\", true)\n sealedTokens[hoveredObject.getGUID()] = {}\nend\n\nfunction sealToken(type, playerColor, hoveredObject)\n local chaosBag = chaosBagApi.findChaosBag()\n\n for i, token in ipairs(chaosBag.getObjects()) do\n if token.name == type then\n return chaosBag.takeObject({\n position = hoveredObject.getPosition() + Vector(0, 1, 0),\n index = i - 1,\n smooth = false,\n callback_function = function(obj)\n table.insert(sealedTokens[hoveredObject.getGUID()], obj)\n table.insert(tokensTaken[type], obj.getGUID())\n tokenArrangerApi.layout()\n updateDisplayAndBroadcast(type)\n end\n })\n end\n end\n printToColor(type .. \" token not found in bag\", playerColor)\nend\n\nfunction releaseToken(type, playerColor, hoveredObject)\n local chaosBag = chaosBagApi.findChaosBag()\n local tokens = sealedTokens[hoveredObject.getGUID()]\n if tokens == nil or #tokens == 0 then return end\n\n for i, token in ipairs(tokens) do\n if token ~= nil and token.getName() == type then\n local guid = token.getGUID()\n chaosBag.putObject(token)\n for j, v in ipairs(tokensTaken[type]) do\n if v == guid then\n table.remove(tokensTaken[type], j)\n table.remove(tokens, i)\n tokenArrangerApi.layout()\n updateDisplayAndBroadcast(type)\n return\n end\n end\n end\n end\n printToColor(type .. \" token not sealed on \" .. hoveredObject.getName(), playerColor)\nend\n\nfunction none() end\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"chaosbag/BlessCurseManager\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"chaosbag/BlessCurseManager\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\n-- common button parameters\nlocal buttonParamaters = {}\nbuttonParamaters.function_owner = self\nbuttonParamaters.color = { 0, 0, 0, 0 }\nbuttonParamaters.width = 700\nbuttonParamaters.height = 700\n\nlocal updating\n\n---------------------------------------------------------\n-- creating buttons and menus + initializing tables\n---------------------------------------------------------\n\nfunction onLoad()\n -- index: 0 - bless\n buttonParamaters.click_function = \"clickBless\"\n buttonParamaters.position = { -1.03, 0, 0.46 }\n buttonParamaters.tooltip = \"Add / Remove Bless\"\n self.createButton(buttonParamaters)\n\n -- index: 1 - curse\n buttonParamaters.click_function = \"clickCurse\"\n buttonParamaters.position[1] = -buttonParamaters.position[1]\n buttonParamaters.tooltip = \"Add / Remove Curse\"\n self.createButton(buttonParamaters)\n\n -- index: 2 - bless count\n buttonParamaters.tooltip = \"\"\n buttonParamaters.click_function = \"none\"\n buttonParamaters.width = 0\n buttonParamaters.height = 0\n buttonParamaters.color = { 0.4, 0.4, 0.4 }\n buttonParamaters.font_color = { 1, 1, 1 }\n buttonParamaters.font_size = 235\n buttonParamaters.position = { -1.03, 0.06, -0.8 }\n self.createButton(buttonParamaters)\n\n -- index: 3 - curse count\n buttonParamaters.position[1] = -buttonParamaters.position[1]\n self.createButton(buttonParamaters)\n\n -- context menu\n self.addContextMenuItem(\"Remove all\", doRemove)\n self.addContextMenuItem(\"Reset\", doReset)\n\n -- initializing tables\n initializeState()\n broadcastCount(\"Curse\")\n broadcastCount(\"Bless\")\nend\n\nfunction resetTables()\n numInPlay = { Bless = 0, Curse = 0 }\n tokensTaken = { Bless = {}, Curse = {} }\n sealedTokens = {}\nend\n\nfunction initializeState()\n resetTables()\n\n -- count tokens in the bag\n local chaosBag = chaosBagApi.findChaosBag()\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == \"Bless\" then\n numInPlay.Bless = numInPlay.Bless + 1\n elseif v.name == \"Curse\" then\n numInPlay.Curse = numInPlay.Curse + 1\n end\n end\n\n -- find tokens in the play area\n for _, obj in ipairs(getObjects()) do\n local pos = obj.getPosition()\n if obj.hasTag(\"tempToken\") then\n -- skip the tokens from the Token Arranger\n elseif pos.x \u003e -65 and pos.x \u003c 10 and pos.z \u003e -35 and pos.z \u003c 35 and obj.type == \"Tile\" then\n if obj.getName() == \"Bless\" then\n table.insert(tokensTaken.Bless, obj.getGUID())\n numInPlay.Bless = numInPlay.Bless + 1\n elseif obj.getName() == \"Curse\" then\n table.insert(tokensTaken.Curse, obj.getGUID())\n numInPlay.Curse = numInPlay.Curse + 1\n end\n end\n end\n\n updateButtonLabels()\nend\n\nfunction updateButtonLabels()\n self.editButton({ index = 2, label = formatTokenCount(\"Bless\", true)})\n self.editButton({ index = 3, label = formatTokenCount(\"Curse\", true)})\nend\n\nfunction broadcastCount(token)\n local count = formatTokenCount(token)\n if count == \"(0 + 0)\" then return end\n broadcastToAll(token .. \" Tokens \" .. count, \"White\")\nend\n\nfunction broadcastStatus(color)\n broadcastToColor(\"Curse Tokens \" .. formatTokenCount(\"Curse\"), color, \"White\")\n broadcastToColor(\"Bless Tokens \" .. formatTokenCount(\"Bless\"), color, \"White\")\nend\n\n---------------------------------------------------------\n-- TTS event handling\n---------------------------------------------------------\n\n-- enable tracking of bag changes for 1 second\nfunction onObjectDrop(_, object)\n if not isBlurseToken(object) then return end\n\n -- check if object was dropped in chaos bag area\n for _, zone in ipairs(object.getZones()) do\n if zone.getName() == \"ChaosBagZone\" then\n trackBagChange = true\n Wait.time(function() trackBagChange = false end, 1)\n return\n end\n end\nend\n\n-- handle manual returning of bless / curse tokens\nfunction onObjectEnterContainer(container, object)\n if not (trackBagChange and isChaosbag(container) and isBlurseToken(object)) then return end\n\n -- small delay to ensure token has entered bag\n Wait.time(doReset, 0.5)\nend\n\nfunction isChaosbag(obj)\n if obj.getDescription() ~= \"Chaos Bag\" then return end -- early exit for performance\n return obj == chaosBagApi.findChaosBag()\nend\n\nfunction isBlurseToken(obj)\n local name = obj.getName()\n return name == \"Bless\" or name == \"Curse\"\nend\n\n---------------------------------------------------------\n-- context menu functions\n---------------------------------------------------------\n\nfunction doRemove(color)\n local chaosBag = chaosBagApi.findChaosBag()\n\n -- remove tokens from chaos bag\n local count = { Bless = 0, Curse = 0 }\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == \"Bless\" or v.name == \"Curse\" then\n chaosBag.takeObject({\n guid = v.guid,\n position = { 0, 5, 0 },\n callback_function = function(obj) obj.destruct() end\n })\n count[v.name] = count[v.name] + 1\n end\n end\n\n broadcastToColor(\"Removed \" .. count.Bless .. \" Bless and \" .. count.Curse .. \" Curse tokens from the chaos bag.\", color, \"White\")\n broadcastToColor(\"Removed \" .. removeTakenTokens(\"Bless\") .. \" Bless and \" .. removeTakenTokens(\"Curse\") .. \" Curse tokens from play.\", color, \"White\")\n\n resetTables()\n updateButtonLabels()\n tokenArrangerApi.layout()\nend\n\nfunction doReset()\n initializeState()\n broadcastCount(\"Curse\")\n broadcastCount(\"Bless\")\n tokenArrangerApi.layout()\nend\n\n---------------------------------------------------------\n-- click functions\n---------------------------------------------------------\n\nfunction clickBless(_, color, isRightClick)\n playerColor = color\n callFunctions(\"Bless\", isRightClick)\nend\n\nfunction clickCurse(_, color, isRightClick)\n playerColor = color\n callFunctions(\"Curse\", isRightClick)\nend\n\nfunction callFunctions(type, isRightClick)\n if not chaosBagApi.canTouchChaosTokens() then return end\n\n if isRightClick then\n removeToken(type)\n else\n addToken(type)\n end\n\n tokenArrangerApi.layout()\nend\n\n---------------------------------------------------------\n-- called functions\n---------------------------------------------------------\n\n-- returns a formatted string with information about the provided token type (bless / curse)\n---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n---@param omitBrackets? boolean Controls whether the brackets should be omitted from the return\n---@return string tokenCount\nfunction formatTokenCount(type, omitBrackets)\n if omitBrackets then\n return (numInPlay[type] - #tokensTaken[type]) .. \" + \" .. #tokensTaken[type]\n else\n return \"(\" .. (numInPlay[type] - #tokensTaken[type]) .. \" + \" .. #tokensTaken[type] .. \")\"\n end\nend\n\n-- seals a token on a card (called by cards that seal bless/curse tokens)\n---@param param table This contains the type and guid of the sealed token\nfunction sealedToken(param)\n table.insert(tokensTaken[param.type], param.guid)\n broadcastCount(param.type)\n updateButtonLabels()\nend\n\n-- returns a token to the bag (called by cards that seal bless/curse tokens)\n---@param param table This contains the type and guid of the released token\nfunction releasedToken(param)\n for i, v in ipairs(tokensTaken[param.type]) do\n if v == param.guid then\n table.remove(tokensTaken[param.type], i)\n break\n end\n end\n \n if not param.fromBag then\n updateDisplayAndBroadcast(param.type)\n end\nend\n\n-- removes a token (called by cards that seal bless/curse tokens)\n---@param param table This contains the type and guid of the released token\nfunction returnedToken(param)\n for i, v in ipairs(tokensTaken[param.type]) do\n if v == param.guid then\n table.remove(tokensTaken[param.type], i)\n numInPlay[param.type] = numInPlay[param.type] - 1\n break\n end\n end\n updateDisplayAndBroadcast(param.type)\nend\n\nfunction updateDisplayAndBroadcast(type)\n if not updating then\n updating = true\n Wait.frames(function()\n broadcastCount(type)\n updateButtonLabels()\n updating = false\n end, 5)\n end\nend\n\nfunction getBlessCurseInBag()\n local numInBag = { Bless = 0, Curse = 0 }\n local chaosBag = chaosBagApi.findChaosBag()\n\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == \"Bless\" or v.name == \"Curse\" then\n numInBag[v.name] = numInBag[v.name] + 1\n end\n end\n\n return numInBag\nend\n\n---------------------------------------------------------\n-- main functions: add and remove\n---------------------------------------------------------\n\nfunction addToken(type)\n if numInPlay[type] == 10 then\n printToColor(\"10 tokens already in play, not adding any.\", playerColor)\n return\n end\n numInPlay[type] = numInPlay[type] + 1\n printToAll(\"Adding \" .. type .. \" token \" .. formatTokenCount(type))\n updateButtonLabels()\n return chaosBagApi.spawnChaosToken(type)\nend\n\nfunction removeToken(type)\n local chaosBag = chaosBagApi.findChaosBag()\n local tokens = {}\n\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == type then\n table.insert(tokens, v.guid)\n end\n end\n\n if #tokens == 0 then\n printToColor(\"No \" .. type .. \" tokens in the chaos bag.\", playerColor)\n return\n end\n\n chaosBag.takeObject({\n guid = table.remove(tokens),\n smooth = false,\n callback_function = function(obj)\n numInPlay[type] = numInPlay[type] - 1\n printToAll(\"Removing \" .. type .. \" token \" .. formatTokenCount(type))\n updateButtonLabels()\n obj.destruct()\n end\n })\nend\n\n-- removing tokens that were 'taken'\nfunction removeTakenTokens(type)\n local count = 0\n for _, guid in ipairs(tokensTaken[type]) do\n local token = getObjectFromGUID(guid)\n if token ~= nil then\n token.destruct()\n count = count + 1\n end\n end\n return count\nend\n\n---------------------------------------------------------\n-- Wendy's Menu (context menu for cards on hotkey press)\n---------------------------------------------------------\n\nfunction addMenuOptions(parameters)\n local playerColor = parameters.playerColor\n local hoveredObject = parameters.hoveredObject\n if hoveredObject == nil or hoveredObject.type ~= \"Card\" then\n broadcastToColor(\"Right-click seal options can only be added to cards.\", playerColor)\n return\n elseif hoveredObject.hasTag(\"CardThatSeals\") or hoveredObject.getVar(\"MENU_ADDED\") == true then\n broadcastToColor(\"This card already has a sealing context menu.\", playerColor)\n return\n end\n\n hoveredObject.addContextMenuItem(\"Seal Bless\", function(color)\n sealToken(\"Bless\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n hoveredObject.addContextMenuItem(\"Release Bless\", function(color)\n releaseToken(\"Bless\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n hoveredObject.addContextMenuItem(\"Seal Curse\", function(color)\n sealToken(\"Curse\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n hoveredObject.addContextMenuItem(\"Release Curse\", function(color)\n releaseToken(\"Curse\", color, hoveredObject)\n tokenArrangerApi.layout()\n end, true)\n\n broadcastToColor(\"Right-click seal options added to \" .. hoveredObject.getName(), playerColor)\n hoveredObject.setVar(\"MENU_ADDED\", true)\n sealedTokens[hoveredObject.getGUID()] = {}\nend\n\nfunction sealToken(type, playerColor, hoveredObject)\n local chaosBag = chaosBagApi.findChaosBag()\n\n for i, token in ipairs(chaosBag.getObjects()) do\n if token.name == type then\n return chaosBag.takeObject({\n position = hoveredObject.getPosition() + Vector(0, 1, 0),\n index = i - 1,\n smooth = false,\n callback_function = function(obj)\n table.insert(sealedTokens[hoveredObject.getGUID()], obj)\n table.insert(tokensTaken[type], obj.getGUID())\n tokenArrangerApi.layout()\n updateDisplayAndBroadcast(type)\n end\n })\n end\n end\n printToColor(type .. \" token not found in bag\", playerColor)\nend\n\nfunction releaseToken(type, playerColor, hoveredObject)\n local chaosBag = chaosBagApi.findChaosBag()\n local tokens = sealedTokens[hoveredObject.getGUID()]\n if tokens == nil or #tokens == 0 then return end\n\n for i, token in ipairs(tokens) do\n if token ~= nil and token.getName() == type then\n local guid = token.getGUID()\n chaosBag.putObject(token)\n for j, v in ipairs(tokensTaken[type]) do\n if v == guid then\n table.remove(tokensTaken[type], j)\n table.remove(tokens, i)\n tokenArrangerApi.layout()\n updateDisplayAndBroadcast(type)\n return\n end\n end\n end\n end\n printToColor(type .. \" token not sealed on \" .. hoveredObject.getName(), playerColor)\nend\n\nfunction none() end\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "false", "MeasureMovement": false, "Name": "Custom_Token", @@ -49893,97 +30206,6 @@ "Value": 0, "XmlUI": "" }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "lua setNotes(getObjectFromGUID('the objects guid').getJSON())", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d8d357", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "", - "Snap": true, - "Sticky": true, - "Tooltip": false, - "Transform": { - "posX": 78, - "posY": 1.244, - "posZ": 33.583, - "rotX": 0, - "rotY": 90, - "rotZ": 0, - "scaleX": 0.25, - "scaleY": 0.25, - "scaleZ": 0.25 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "a": 0.5098, - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a2f932", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "ScriptingTrigger", - "Nickname": "", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -27.94, - "posY": 3.5, - "posZ": 0, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 37, - "scaleY": 4, - "scaleZ": 37 - }, - "Value": 0, - "XmlUI": "" - }, { "AltLookAngle": { "x": 0, @@ -49999,7 +30221,7 @@ }, "CustomMesh": { "CastShadows": true, - "ColliderURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_COL.obj", + "ColliderURL": "", "Convex": true, "CustomShader": { "FresnelStrength": 0, @@ -50028,7 +30250,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -50041,7 +30263,7 @@ "Tooltip": true, "Transform": { "posX": 20.6, - "posY": 1.486, + "posY": 1.481, "posZ": -65, "rotX": 0, "rotY": 270, @@ -50068,7 +30290,7 @@ }, "CustomMesh": { "CastShadows": true, - "ColliderURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_COL.obj", + "ColliderURL": "", "Convex": true, "CustomShader": { "FresnelStrength": 0, @@ -50097,7 +30319,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -50110,7 +30332,7 @@ "Tooltip": true, "Transform": { "posX": 35.4, - "posY": 1.486, + "posY": 1.481, "posZ": -78, "rotX": 0, "rotY": 270, @@ -50137,7 +30359,7 @@ }, "CustomMesh": { "CastShadows": true, - "ColliderURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_COL.obj", + "ColliderURL": "", "Convex": true, "CustomShader": { "FresnelStrength": 0, @@ -50166,7 +30388,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -50179,7 +30401,7 @@ "Tooltip": true, "Transform": { "posX": 20.6, - "posY": 1.486, + "posY": 1.481, "posZ": -91, "rotX": 0, "rotY": 270, @@ -50199,9 +30421,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "a": 0.27451, + "a": 0.27, "b": 1, - "g": 0.99608, + "g": 1, "r": 1 }, "CustomMesh": { @@ -50235,7 +30457,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -50295,7 +30517,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DoomInPlayCounter\")\nend)\n__bundle_register(\"core/DoomInPlayCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nlocal ZONE, TRASH\nlocal doomURL = \"https://i.imgur.com/EoL7yaZ.png\"\nlocal IGNORE_TAG = \"DoomCounter_ignore\"\nlocal TOTAL_PLAY_AREA = {\n upperLeft = {\n x = -9,\n z = -35\n },\n lowerRight = {\n x = -60,\n z = 35\n }\n}\n\n-- create button, context menu and start loop\nfunction onLoad()\n self.createButton({\n label = \"0\",\n click_function = \"none\",\n function_owner = self,\n position = { 0, 0.06, 0 },\n height = 0,\n width = 0,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n TRASH = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n ZONE = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaZone\")\n Wait.time(updateCounter, 2, -1)\nend\n\n-- main function\nfunction updateCounter()\n local count = countDoomInPlay()\n self.editButton({ index = 0, label = tostring(count) })\nend\n\n-- get doom in play\nfunction countDoomInPlay()\n local count = 0\n\n for _, obj in ipairs(getObjects()) do\n count = count + getDoomAmount(obj)\n end\n\n return count\nend\n\n-- gets quantity (for stacks) of doom\nfunction getDoomAmount(obj)\n if (obj.is_face_down and obj.getCustomObject().image_bottom == doomURL)\n and not obj.hasTag(IGNORE_TAG)\n and inArea(obj.getPosition(), TOTAL_PLAY_AREA) then\n return math.abs(obj.getQuantity())\n else\n return 0\n end\nend\n\n-- removes doom from playermats / playarea\nfunction removeDoom(options)\n if options.Playermats then\n local count = removeDoomFromList(playmatApi.searchAroundPlaymat(\"All\"))\n if count \u003e 0 then \n broadcastToAll(count .. \" doom removed from playermats.\", \"White\")\n end\n end\n\n if options.Playarea then\n local count = removeDoomFromList(ZONE.getObjects())\n if count \u003e 0 then \n broadcastToAll(count .. \" doom removed from play area.\", \"White\")\n end\n end\nend\n\n-- removes doom from provided object list and returns the removed amount\nfunction removeDoomFromList(objList)\n local count = 0\n for _, obj in ipairs(objList) do\n local amount = getDoomAmount(obj)\n if amount \u003e 0 then\n TRASH.putObject(obj)\n count = count + amount\n end\n end\n return count\nend\n\n-- helper function to check if a position is inside an area\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003e bounds.upperLeft.z\n and point.z \u003c bounds.lowerRight.z)\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DoomInPlayCounter\")\nend)\n__bundle_register(\"core/DoomInPlayCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nlocal ZONE, TRASH\nlocal doomURL = \"https://i.imgur.com/EoL7yaZ.png\"\nlocal IGNORE_TAG = \"DoomCounter_ignore\"\nlocal TOTAL_PLAY_AREA = {\n upperLeft = {\n x = -9,\n z = -35\n },\n lowerRight = {\n x = -60,\n z = 35\n }\n}\n\n-- create button, context menu and start loop\nfunction onLoad()\n self.createButton({\n label = \"0\",\n click_function = \"none\",\n function_owner = self,\n position = { 0, 0.06, 0 },\n height = 0,\n width = 0,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n TRASH = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n ZONE = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaZone\")\n Wait.time(updateCounter, 2, -1)\nend\n\n-- main function\nfunction updateCounter()\n local count = countDoomInPlay()\n self.editButton({ index = 0, label = tostring(count) })\nend\n\n-- get doom in play\nfunction countDoomInPlay()\n local count = 0\n\n for _, obj in ipairs(getObjects()) do\n count = count + getDoomAmount(obj)\n end\n\n return count\nend\n\n-- gets quantity (for stacks) of doom\nfunction getDoomAmount(obj)\n if (obj.is_face_down and obj.getCustomObject().image_bottom == doomURL)\n and not obj.hasTag(IGNORE_TAG)\n and inArea(obj.getPosition(), TOTAL_PLAY_AREA) then\n return math.abs(obj.getQuantity())\n else\n return 0\n end\nend\n\n-- removes doom from playermats / playarea\nfunction removeDoom(options)\n if options.Playermats then\n local count = removeDoomFromList(playermatApi.searchAroundPlayermat(\"All\"))\n if count \u003e 0 then\n broadcastToAll(count .. \" doom removed from playermats.\", \"White\")\n end\n end\n\n if options.Playarea then\n local count = removeDoomFromList(ZONE.getObjects())\n if count \u003e 0 then\n broadcastToAll(count .. \" doom removed from play area.\", \"White\")\n end\n end\nend\n\n-- removes doom from provided object list and returns the removed amount\nfunction removeDoomFromList(objList)\n local count = 0\n for _, obj in ipairs(objList) do\n local amount = getDoomAmount(obj)\n if amount \u003e 0 then\n TRASH.putObject(obj)\n count = count + amount\n end\n end\n return count\nend\n\n-- helper function to check if a position is inside an area\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003e bounds.upperLeft.z\n and point.z \u003c bounds.lowerRight.z)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Token", @@ -50334,47 +30556,57 @@ "z": 0.118 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -0.865, + "x": -0.86, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1, + "x": -1.03, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.18, + "x": -1.2, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.36, + "x": -1.37, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" + ] + }, + { + "Position": { + "x": -1.54, + "y": 0.1, + "z": -0.28 + }, + "Tags": [ + "UniversalToken" ] }, { @@ -50665,8 +30897,8 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/Playmat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playmat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right)\nlocal slotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variable so it can be reset by the Clean Up Helper\nactiveInvestigatorId = \"00000\"\nlocal isDrawButtonVisible = false\n\n-- global variable to report \"Dream-Enhancing Serum\" status\nisDES = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n playerColor = playerColor,\n activeInvestigatorId = activeInvestigatorId,\n isDrawButtonVisible = isDrawButtonVisible,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n playerColor = loadedData.playerColor\n activeInvestigatorId = loadedData.activeInvestigatorId\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n slotData = loadedData.slotData\n end\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playmat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local bounds = self.getBoundsNormalized()\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge\n -- of the cast at the edge of the playmat\n -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the\n -- table position of the playmat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n\n -- put chaos tokens back into bag (e.g. Unrelenting)\n elseif tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find forcedLearning\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.getDescription() == \"Action Token\" and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or false) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect forced learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards\n if cardMetadata.uses ~= nil then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n local handSize = #Player[playerColor].getHandObjects()\n if handSize \u003c 5 then\n local cardsToDraw = 5 - handSize\n printToColor(\"Drawing \" .. cardsToDraw .. \" cards (Patrice)\", messageColor)\n drawCardsWithReshuffle(cardsToDraw)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n for i = 1, #hand do\n local notes = JSON.decode(hand[i].getGMNotes())\n if notes ~= nil then\n if notes.hidden ~= true then\n table.insert(choices, i)\n end\n else\n table.insert(choices, i)\n end\n end\n\n if #choices == 0 then\n broadcastToColor(\"Hidden cards can't be randomly discarded.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random non-hidden card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n\n local playerName = Player[playerColor].steam_name or playerColor\n broadcastToAll(playerName .. \" randomly discarded card \" .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playmat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n -- detect if \"Dream-Enhancing Serum\" is placed\n if object.getName() == \"Dream-Enhancing Serum\" then isDES = true end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- detect if \"Dream-Enhancing Serum\" is removed\nfunction onCollisionExit(collisionInfo)\n if collisionInfo.collision_object.getName() == \"Dream-Enhancing Serum\" then isDES = false end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false and\n obj.getDescription() ~= \"Action Token\" then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local class\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n class = notes.class\n activeInvestigatorId = notes.id\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n elseif activeInvestigatorId ~= \"00000\" then\n class = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n return\n end\n\n -- change state of action tokens\n local search = searchArea(self.positionToWorld({ -1.1, 0.05, -0.27 }), { 4, 1, 1 })\n local smallToken = nil\n local STATE_TABLE = {\n [\"Guardian\"] = 1,\n [\"Seeker\"] = 2,\n [\"Rogue\"] = 3,\n [\"Mystic\"] = 4,\n [\"Survivor\"] = 5,\n [\"Neutral\"] = 6\n }\n\n for _, obj in ipairs(search) do\n if obj.getDescription() == \"Action Token\" and obj.getStateId() \u003e 0 then\n if obj.getScale().x \u003c 0.4 then\n smallToken = obj\n else\n setObjectState(obj, STATE_TABLE[class])\n end\n end\n end\n\n -- update the small token with special action for certain investigators\n local SPECIAL_ACTIONS = {\n [\"04002\"] = 8, -- Ursula Downs\n [\"01002\"] = 9, -- Daisy Walker\n [\"01502\"] = 9, -- Daisy Walker\n [\"01002-pb\"] = 9, -- Daisy Walker\n [\"06003\"] = 10, -- Tony Morgan\n [\"04003\"] = 11, -- Finn Edwards\n [\"08016\"] = 14 -- Bob Jenkins\n }\n\n if smallToken ~= nil then\n setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])\n end\nend\n\nfunction setObjectState(obj, stateId)\n if obj.getStateId() ~= stateId then obj.setState(stateId) end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playmat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playmat\")\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "{\"activeInvestigatorId\":\"00000\",\"isDrawButtonVisible\":false,\"playerColor\":\"White\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playermat\")\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"playermat/Playermat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\nlocal SEARCH_AROUND_SELF_Z_BUFFER = 1.75\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playermat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- table of texture URLs\nlocal nameToTexture = {\n Guardian = \"http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/\",\n Mystic = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/\",\n Neutral = \"http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/\",\n Rogue = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/\",\n Seeker = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/\",\n Survivor = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/\"\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right) - intentionally global!\nslotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variables for access\nactiveInvestigatorClass = \"Neutral\"\nactiveInvestigatorId = \"00000\"\nhasDES = false\n\nlocal isClassTextureEnabled = true\nlocal isDrawButtonVisible = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n activeInvestigatorClass = activeInvestigatorClass,\n activeInvestigatorId = activeInvestigatorId,\n isClassTextureEnabled = isClassTextureEnabled,\n isDrawButtonVisible = isDrawButtonVisible,\n playerColor = playerColor,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n activeInvestigatorClass = loadedData.activeInvestigatorClass\n activeInvestigatorId = loadedData.activeInvestigatorId\n isClassTextureEnabled = loadedData.isClassTextureEnabled\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n playerColor = loadedData.playerColor\n slotData = loadedData.slotData\n end\n\n updateMessageColor(playerColor)\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playermat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER\n\n -- 'setAsideDirection' accounts for the set aside zone being on the left or right,\n -- depending on the table position of the playermat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n\n -- Since the cast is centered on the position, shift left or right to keep\n -- the non-set aside edge of the cast at the edge of the playermat\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / scale.x\n localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n elseif tokenChecker.isChaosToken(obj) then\n -- put chaos tokens back into bag (e.g. Unrelenting)\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum\n checkForDES()\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.hasTag(\"Temporary\") == true then\n discardListOfObjects({ obj })\n elseif obj.hasTag(\"UniversalToken\") == true and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or obj.hasTag(\"DoNotReady\")) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect Forced Learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile)\n if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x \u003e -1 then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n -- discards all non-weakness and non-hidden cards from hand first\n local handCards = Player[playerColor].getHandObjects()\n local cardsToDiscard = {}\n\n for _, card in ipairs(handCards) do\n local md = JSON.decode(card.getGMNotes())\n if card.type == \"Card\" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= \"52020\") then\n table.insert(cardsToDiscard, card)\n end\n end\n\n -- perform discarding 1 by 1\n local pos = returnGlobalDiscardPosition()\n deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation())\n\n -- draw up to 5 cards\n local cardsToDraw = 5 - #handCards + #cardsToDiscard\n if cardsToDraw \u003e 0 then\n printToColor(\"Discarding \" .. #cardsToDiscard .. \" and drawing \" .. cardsToDraw .. \" card(s). (Patrice)\", messageColor)\n\n -- add some time if there are any cards to discard\n local k = 0\n if #cardsToDiscard \u003e 0 then\n k = 0.8 + (#cardsToDiscard * 0.1)\n end\n Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n local hiddenCards = {}\n local missingMetadataCards = {}\n for i, handObj in ipairs(hand) do\n if handObj.type == \"Card\" then\n -- get a name for the card or use the index if unnamed\n local name = handObj.getName()\n if name == \"\" then\n name = \"Card \" .. i\n end\n\n -- check card for metadata\n local md = JSON.decode(handObj.getGMNotes())\n if md == nil then\n table.insert(missingMetadataCards, name)\n elseif md.hidden or md.id == \"52020\" then\n table.insert(hiddenCards, name)\n else\n table.insert(choices, i)\n end\n end\n end\n\n -- print message with hidden cards\n if #hiddenCards \u003e 0 then\n local cardList = concatenateListOfStrings(hiddenCards)\n printToColor(\"Excluded (hidden): \" .. cardList, messageColor)\n end\n\n -- print message with missing metadata cards\n if #missingMetadataCards \u003e 0 then\n local cardList = concatenateListOfStrings(missingMetadataCards)\n printToColor(\"Excluded (missing data): \" .. cardList, messageColor)\n end\n\n if #choices == 0 then\n broadcastToColor(\"Didn't find any eligible cards for random discarding.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random eligible card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n broadcastToAll(getColoredName(playerColor) .. \" randomly discarded card \"\n .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\nfunction concatenateListOfStrings(list)\n local cardList\n for _, cardName in ipairs(list) do\n if not cardList then\n cardList = \"\"\n else\n cardList = cardList .. \", \"\n end\n cardList = cardList .. cardName\n end\n return cardList\nend\n\n-- checks if DES is present\nfunction checkForDES()\n hasDES = false\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.type == \"Card\" then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n\n -- position is used to exclude deck / discard\n local cardPos = self.positionToLocal(obj.getPosition())\n if cardMetadata.id == \"06159\" and cardPos.x \u003e -1 then\n hasDES = true\n break\n end\n end\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playermat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local extraToken\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n activeInvestigatorClass = notes.class\n activeInvestigatorId = notes.id\n extraToken = notes.extraToken\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n updateTexture()\n elseif activeInvestigatorId ~= \"00000\" then\n activeInvestigatorClass = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n updateTexture()\n else\n return\n end\n\n -- set proper scale for investigators\n local cardData = card.getData()\n if cardData[\"SidewaysCard\"] == true then\n -- 115% for easier readability\n card.setScale({ 1.15, 1, 1.15 })\n else\n -- Zoop-exported investigators are horizontal cards and TTS scales them differently\n card.setScale({ 0.8214, 1, 0.8214 })\n end\n\n -- remove old action tokens\n for _, obj in ipairs(searchAroundSelf(\"isUniversalToken\")) do\n obj.destruct()\n end\n\n -- spawn three regular action tokens (investigator specific one in the bottom spot)\n for i = 1, 3 do\n local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(pos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = activeInvestigatorClass })\n end)\n end\n\n -- spawn additional token (maybe specific for investigator)\n if extraToken and extraToken ~= \"None\" then\n -- local positions\n local tokenSpawnPos = {\n action = {\n Vector(-0.86, 0, -0.28), -- left of the regular three actions\n Vector(-1.54, 0, -0.28), -- right of the regular three actions\n },\n ability = {\n Vector(-1, 0, 0.118), -- bottom left corner of the investigator card\n Vector(-1, 0, -0.118), -- top left corner of the investigator card\n }\n }\n\n -- spawn tokens (split string by \"|\")\n local count = { action = 0, ability = 0 }\n for str in string.gmatch(extraToken, \"([^|]+)\") do\n local type = \"action\"\n if str == \"FreeTrigger\" or str == \"Reaction\" then\n type = \"ability\"\n end\n\n count[type] = count[type] + 1\n if count[type] \u003e 2 then\n printToColor(\"More than two extra tokens of the same type are not supported.\", playerColor)\n else\n local localSpawnPos = tokenSpawnPos[type][count[type]]\n local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(globalSpawnPos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = str })\n end)\n end\n end\n end\nend\n\n-- updates the texture of the playermat\n---@param overrideName? string Force a specific texture\nfunction updateTexture(overrideName)\n local name = \"Neutral\"\n\n -- use class specific texture if enabled\n if isClassTextureEnabled then\n name = activeInvestigatorClass\n end\n\n -- get new texture URL\n local newUrl = nameToTexture[name]\n\n -- override name if valid\n if nameToTexture[overrideName] then\n newUrl = nameToTexture[overrideName]\n end\n\n -- apply texture\n local customInfo = self.getCustomObject()\n if customInfo.image ~= newUrl then\n -- temporarily lock objects so they don't fall through the mat\n local objectsToUnlock = {}\n for _, obj in ipairs(searchAroundSelf()) do\n if not obj.getLock() then\n obj.setLock(true)\n table.insert(objectsToUnlock, obj)\n end\n end\n\n self.script_state = onSave()\n customInfo.image = newUrl\n self.setCustomObject(customInfo)\n local reloadedMat = self.reload()\n\n -- unlock objects when mat is reloaded\n Wait.condition(function()\n for _, obj in ipairs(objectsToUnlock) do\n obj.setLock(false)\n end\n end, function() return reloadedMat.loading_custom == false end)\n end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playermat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- Toggles the use of class textures\n---@param state boolean Whether the class texture should be used or not\nfunction useClassTexture(state)\n if state == isClassTextureEnabled then return end\n isClassTextureEnabled = state\n updateTexture()\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\n\n-- returns the colored steam name or color\nfunction getColoredName(playerColor)\n local displayName = playerColor\n if Player[playerColor].steam_name then\n displayName = Player[playerColor].steam_name\n end\n\n -- add bb-code\n return \"[\" .. Color.fromString(playerColor):toHex() .. \"]\" .. displayName .. \"[-]\"\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"activeInvestigatorClass\":\"Neutral\",\"activeInvestigatorId\":\"00000\",\"isClassTextureEnabled\":true,\"isDrawButtonVisible\":false,\"playerColor\":\"White\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", "MeasureMovement": false, "Memo": "White", "Name": "Custom_Tile", @@ -50702,47 +30934,57 @@ "z": 0.118 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -0.865, + "x": -0.86, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1, + "x": -1.03, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.18, + "x": -1.2, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.36, + "x": -1.37, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" + ] + }, + { + "Position": { + "x": -1.54, + "y": 0.1, + "z": -0.28 + }, + "Tags": [ + "UniversalToken" ] }, { @@ -51033,8 +31275,8 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playmat\")\nend)\n__bundle_register(\"playermat/Playmat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playmat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right)\nlocal slotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variable so it can be reset by the Clean Up Helper\nactiveInvestigatorId = \"00000\"\nlocal isDrawButtonVisible = false\n\n-- global variable to report \"Dream-Enhancing Serum\" status\nisDES = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n playerColor = playerColor,\n activeInvestigatorId = activeInvestigatorId,\n isDrawButtonVisible = isDrawButtonVisible,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n playerColor = loadedData.playerColor\n activeInvestigatorId = loadedData.activeInvestigatorId\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n slotData = loadedData.slotData\n end\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playmat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local bounds = self.getBoundsNormalized()\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge\n -- of the cast at the edge of the playmat\n -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the\n -- table position of the playmat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n\n -- put chaos tokens back into bag (e.g. Unrelenting)\n elseif tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find forcedLearning\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.getDescription() == \"Action Token\" and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or false) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect forced learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards\n if cardMetadata.uses ~= nil then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n local handSize = #Player[playerColor].getHandObjects()\n if handSize \u003c 5 then\n local cardsToDraw = 5 - handSize\n printToColor(\"Drawing \" .. cardsToDraw .. \" cards (Patrice)\", messageColor)\n drawCardsWithReshuffle(cardsToDraw)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n for i = 1, #hand do\n local notes = JSON.decode(hand[i].getGMNotes())\n if notes ~= nil then\n if notes.hidden ~= true then\n table.insert(choices, i)\n end\n else\n table.insert(choices, i)\n end\n end\n\n if #choices == 0 then\n broadcastToColor(\"Hidden cards can't be randomly discarded.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random non-hidden card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n\n local playerName = Player[playerColor].steam_name or playerColor\n broadcastToAll(playerName .. \" randomly discarded card \" .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playmat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n -- detect if \"Dream-Enhancing Serum\" is placed\n if object.getName() == \"Dream-Enhancing Serum\" then isDES = true end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- detect if \"Dream-Enhancing Serum\" is removed\nfunction onCollisionExit(collisionInfo)\n if collisionInfo.collision_object.getName() == \"Dream-Enhancing Serum\" then isDES = false end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false and\n obj.getDescription() ~= \"Action Token\" then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local class\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n class = notes.class\n activeInvestigatorId = notes.id\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n elseif activeInvestigatorId ~= \"00000\" then\n class = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n return\n end\n\n -- change state of action tokens\n local search = searchArea(self.positionToWorld({ -1.1, 0.05, -0.27 }), { 4, 1, 1 })\n local smallToken = nil\n local STATE_TABLE = {\n [\"Guardian\"] = 1,\n [\"Seeker\"] = 2,\n [\"Rogue\"] = 3,\n [\"Mystic\"] = 4,\n [\"Survivor\"] = 5,\n [\"Neutral\"] = 6\n }\n\n for _, obj in ipairs(search) do\n if obj.getDescription() == \"Action Token\" and obj.getStateId() \u003e 0 then\n if obj.getScale().x \u003c 0.4 then\n smallToken = obj\n else\n setObjectState(obj, STATE_TABLE[class])\n end\n end\n end\n\n -- update the small token with special action for certain investigators\n local SPECIAL_ACTIONS = {\n [\"04002\"] = 8, -- Ursula Downs\n [\"01002\"] = 9, -- Daisy Walker\n [\"01502\"] = 9, -- Daisy Walker\n [\"01002-pb\"] = 9, -- Daisy Walker\n [\"06003\"] = 10, -- Tony Morgan\n [\"04003\"] = 11, -- Finn Edwards\n [\"08016\"] = 14 -- Bob Jenkins\n }\n\n if smallToken ~= nil then\n setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])\n end\nend\n\nfunction setObjectState(obj, stateId)\n if obj.getStateId() ~= stateId then obj.setState(stateId) end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playmat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "{\"activeInvestigatorId\":\"00000\",\"isDrawButtonVisible\":false,\"playerColor\":\"Orange\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playermat\")\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"playermat/Playermat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\nlocal SEARCH_AROUND_SELF_Z_BUFFER = 1.75\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playermat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- table of texture URLs\nlocal nameToTexture = {\n Guardian = \"http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/\",\n Mystic = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/\",\n Neutral = \"http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/\",\n Rogue = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/\",\n Seeker = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/\",\n Survivor = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/\"\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right) - intentionally global!\nslotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variables for access\nactiveInvestigatorClass = \"Neutral\"\nactiveInvestigatorId = \"00000\"\nhasDES = false\n\nlocal isClassTextureEnabled = true\nlocal isDrawButtonVisible = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n activeInvestigatorClass = activeInvestigatorClass,\n activeInvestigatorId = activeInvestigatorId,\n isClassTextureEnabled = isClassTextureEnabled,\n isDrawButtonVisible = isDrawButtonVisible,\n playerColor = playerColor,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n activeInvestigatorClass = loadedData.activeInvestigatorClass\n activeInvestigatorId = loadedData.activeInvestigatorId\n isClassTextureEnabled = loadedData.isClassTextureEnabled\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n playerColor = loadedData.playerColor\n slotData = loadedData.slotData\n end\n\n updateMessageColor(playerColor)\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playermat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER\n\n -- 'setAsideDirection' accounts for the set aside zone being on the left or right,\n -- depending on the table position of the playermat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n\n -- Since the cast is centered on the position, shift left or right to keep\n -- the non-set aside edge of the cast at the edge of the playermat\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / scale.x\n localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n elseif tokenChecker.isChaosToken(obj) then\n -- put chaos tokens back into bag (e.g. Unrelenting)\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum\n checkForDES()\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.hasTag(\"Temporary\") == true then\n discardListOfObjects({ obj })\n elseif obj.hasTag(\"UniversalToken\") == true and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or obj.hasTag(\"DoNotReady\")) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect Forced Learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile)\n if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x \u003e -1 then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n -- discards all non-weakness and non-hidden cards from hand first\n local handCards = Player[playerColor].getHandObjects()\n local cardsToDiscard = {}\n\n for _, card in ipairs(handCards) do\n local md = JSON.decode(card.getGMNotes())\n if card.type == \"Card\" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= \"52020\") then\n table.insert(cardsToDiscard, card)\n end\n end\n\n -- perform discarding 1 by 1\n local pos = returnGlobalDiscardPosition()\n deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation())\n\n -- draw up to 5 cards\n local cardsToDraw = 5 - #handCards + #cardsToDiscard\n if cardsToDraw \u003e 0 then\n printToColor(\"Discarding \" .. #cardsToDiscard .. \" and drawing \" .. cardsToDraw .. \" card(s). (Patrice)\", messageColor)\n\n -- add some time if there are any cards to discard\n local k = 0\n if #cardsToDiscard \u003e 0 then\n k = 0.8 + (#cardsToDiscard * 0.1)\n end\n Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n local hiddenCards = {}\n local missingMetadataCards = {}\n for i, handObj in ipairs(hand) do\n if handObj.type == \"Card\" then\n -- get a name for the card or use the index if unnamed\n local name = handObj.getName()\n if name == \"\" then\n name = \"Card \" .. i\n end\n\n -- check card for metadata\n local md = JSON.decode(handObj.getGMNotes())\n if md == nil then\n table.insert(missingMetadataCards, name)\n elseif md.hidden or md.id == \"52020\" then\n table.insert(hiddenCards, name)\n else\n table.insert(choices, i)\n end\n end\n end\n\n -- print message with hidden cards\n if #hiddenCards \u003e 0 then\n local cardList = concatenateListOfStrings(hiddenCards)\n printToColor(\"Excluded (hidden): \" .. cardList, messageColor)\n end\n\n -- print message with missing metadata cards\n if #missingMetadataCards \u003e 0 then\n local cardList = concatenateListOfStrings(missingMetadataCards)\n printToColor(\"Excluded (missing data): \" .. cardList, messageColor)\n end\n\n if #choices == 0 then\n broadcastToColor(\"Didn't find any eligible cards for random discarding.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random eligible card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n broadcastToAll(getColoredName(playerColor) .. \" randomly discarded card \"\n .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\nfunction concatenateListOfStrings(list)\n local cardList\n for _, cardName in ipairs(list) do\n if not cardList then\n cardList = \"\"\n else\n cardList = cardList .. \", \"\n end\n cardList = cardList .. cardName\n end\n return cardList\nend\n\n-- checks if DES is present\nfunction checkForDES()\n hasDES = false\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.type == \"Card\" then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n\n -- position is used to exclude deck / discard\n local cardPos = self.positionToLocal(obj.getPosition())\n if cardMetadata.id == \"06159\" and cardPos.x \u003e -1 then\n hasDES = true\n break\n end\n end\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playermat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local extraToken\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n activeInvestigatorClass = notes.class\n activeInvestigatorId = notes.id\n extraToken = notes.extraToken\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n updateTexture()\n elseif activeInvestigatorId ~= \"00000\" then\n activeInvestigatorClass = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n updateTexture()\n else\n return\n end\n\n -- set proper scale for investigators\n local cardData = card.getData()\n if cardData[\"SidewaysCard\"] == true then\n -- 115% for easier readability\n card.setScale({ 1.15, 1, 1.15 })\n else\n -- Zoop-exported investigators are horizontal cards and TTS scales them differently\n card.setScale({ 0.8214, 1, 0.8214 })\n end\n\n -- remove old action tokens\n for _, obj in ipairs(searchAroundSelf(\"isUniversalToken\")) do\n obj.destruct()\n end\n\n -- spawn three regular action tokens (investigator specific one in the bottom spot)\n for i = 1, 3 do\n local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(pos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = activeInvestigatorClass })\n end)\n end\n\n -- spawn additional token (maybe specific for investigator)\n if extraToken and extraToken ~= \"None\" then\n -- local positions\n local tokenSpawnPos = {\n action = {\n Vector(-0.86, 0, -0.28), -- left of the regular three actions\n Vector(-1.54, 0, -0.28), -- right of the regular three actions\n },\n ability = {\n Vector(-1, 0, 0.118), -- bottom left corner of the investigator card\n Vector(-1, 0, -0.118), -- top left corner of the investigator card\n }\n }\n\n -- spawn tokens (split string by \"|\")\n local count = { action = 0, ability = 0 }\n for str in string.gmatch(extraToken, \"([^|]+)\") do\n local type = \"action\"\n if str == \"FreeTrigger\" or str == \"Reaction\" then\n type = \"ability\"\n end\n\n count[type] = count[type] + 1\n if count[type] \u003e 2 then\n printToColor(\"More than two extra tokens of the same type are not supported.\", playerColor)\n else\n local localSpawnPos = tokenSpawnPos[type][count[type]]\n local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(globalSpawnPos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = str })\n end)\n end\n end\n end\nend\n\n-- updates the texture of the playermat\n---@param overrideName? string Force a specific texture\nfunction updateTexture(overrideName)\n local name = \"Neutral\"\n\n -- use class specific texture if enabled\n if isClassTextureEnabled then\n name = activeInvestigatorClass\n end\n\n -- get new texture URL\n local newUrl = nameToTexture[name]\n\n -- override name if valid\n if nameToTexture[overrideName] then\n newUrl = nameToTexture[overrideName]\n end\n\n -- apply texture\n local customInfo = self.getCustomObject()\n if customInfo.image ~= newUrl then\n -- temporarily lock objects so they don't fall through the mat\n local objectsToUnlock = {}\n for _, obj in ipairs(searchAroundSelf()) do\n if not obj.getLock() then\n obj.setLock(true)\n table.insert(objectsToUnlock, obj)\n end\n end\n\n self.script_state = onSave()\n customInfo.image = newUrl\n self.setCustomObject(customInfo)\n local reloadedMat = self.reload()\n\n -- unlock objects when mat is reloaded\n Wait.condition(function()\n for _, obj in ipairs(objectsToUnlock) do\n obj.setLock(false)\n end\n end, function() return reloadedMat.loading_custom == false end)\n end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playermat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- Toggles the use of class textures\n---@param state boolean Whether the class texture should be used or not\nfunction useClassTexture(state)\n if state == isClassTextureEnabled then return end\n isClassTextureEnabled = state\n updateTexture()\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\n\n-- returns the colored steam name or color\nfunction getColoredName(playerColor)\n local displayName = playerColor\n if Player[playerColor].steam_name then\n displayName = Player[playerColor].steam_name\n end\n\n -- add bb-code\n return \"[\" .. Color.fromString(playerColor):toHex() .. \"]\" .. displayName .. \"[-]\"\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"activeInvestigatorClass\":\"Neutral\",\"activeInvestigatorId\":\"00000\",\"isClassTextureEnabled\":true,\"isDrawButtonVisible\":false,\"playerColor\":\"Orange\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", "MeasureMovement": false, "Memo": "Orange", "Name": "Custom_Tile", @@ -51070,47 +31312,57 @@ "z": 0.118 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -0.865, + "x": -0.86, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1, + "x": -1.03, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.18, + "x": -1.2, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.36, + "x": -1.37, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" + ] + }, + { + "Position": { + "x": -1.54, + "y": 0.1, + "z": -0.28 + }, + "Tags": [ + "UniversalToken" ] }, { @@ -51401,8 +31653,8 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playmat\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"playermat/Playmat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playmat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right)\nlocal slotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variable so it can be reset by the Clean Up Helper\nactiveInvestigatorId = \"00000\"\nlocal isDrawButtonVisible = false\n\n-- global variable to report \"Dream-Enhancing Serum\" status\nisDES = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n playerColor = playerColor,\n activeInvestigatorId = activeInvestigatorId,\n isDrawButtonVisible = isDrawButtonVisible,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n playerColor = loadedData.playerColor\n activeInvestigatorId = loadedData.activeInvestigatorId\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n slotData = loadedData.slotData\n end\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playmat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local bounds = self.getBoundsNormalized()\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge\n -- of the cast at the edge of the playmat\n -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the\n -- table position of the playmat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n\n -- put chaos tokens back into bag (e.g. Unrelenting)\n elseif tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find forcedLearning\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.getDescription() == \"Action Token\" and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or false) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect forced learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards\n if cardMetadata.uses ~= nil then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n local handSize = #Player[playerColor].getHandObjects()\n if handSize \u003c 5 then\n local cardsToDraw = 5 - handSize\n printToColor(\"Drawing \" .. cardsToDraw .. \" cards (Patrice)\", messageColor)\n drawCardsWithReshuffle(cardsToDraw)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n for i = 1, #hand do\n local notes = JSON.decode(hand[i].getGMNotes())\n if notes ~= nil then\n if notes.hidden ~= true then\n table.insert(choices, i)\n end\n else\n table.insert(choices, i)\n end\n end\n\n if #choices == 0 then\n broadcastToColor(\"Hidden cards can't be randomly discarded.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random non-hidden card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n\n local playerName = Player[playerColor].steam_name or playerColor\n broadcastToAll(playerName .. \" randomly discarded card \" .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playmat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n -- detect if \"Dream-Enhancing Serum\" is placed\n if object.getName() == \"Dream-Enhancing Serum\" then isDES = true end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- detect if \"Dream-Enhancing Serum\" is removed\nfunction onCollisionExit(collisionInfo)\n if collisionInfo.collision_object.getName() == \"Dream-Enhancing Serum\" then isDES = false end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false and\n obj.getDescription() ~= \"Action Token\" then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local class\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n class = notes.class\n activeInvestigatorId = notes.id\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n elseif activeInvestigatorId ~= \"00000\" then\n class = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n return\n end\n\n -- change state of action tokens\n local search = searchArea(self.positionToWorld({ -1.1, 0.05, -0.27 }), { 4, 1, 1 })\n local smallToken = nil\n local STATE_TABLE = {\n [\"Guardian\"] = 1,\n [\"Seeker\"] = 2,\n [\"Rogue\"] = 3,\n [\"Mystic\"] = 4,\n [\"Survivor\"] = 5,\n [\"Neutral\"] = 6\n }\n\n for _, obj in ipairs(search) do\n if obj.getDescription() == \"Action Token\" and obj.getStateId() \u003e 0 then\n if obj.getScale().x \u003c 0.4 then\n smallToken = obj\n else\n setObjectState(obj, STATE_TABLE[class])\n end\n end\n end\n\n -- update the small token with special action for certain investigators\n local SPECIAL_ACTIONS = {\n [\"04002\"] = 8, -- Ursula Downs\n [\"01002\"] = 9, -- Daisy Walker\n [\"01502\"] = 9, -- Daisy Walker\n [\"01002-pb\"] = 9, -- Daisy Walker\n [\"06003\"] = 10, -- Tony Morgan\n [\"04003\"] = 11, -- Finn Edwards\n [\"08016\"] = 14 -- Bob Jenkins\n }\n\n if smallToken ~= nil then\n setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])\n end\nend\n\nfunction setObjectState(obj, stateId)\n if obj.getStateId() ~= stateId then obj.setState(stateId) end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playmat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "{\"activeInvestigatorId\":\"00000\",\"isDrawButtonVisible\":false,\"playerColor\":\"Green\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"playermat/Playermat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\nlocal SEARCH_AROUND_SELF_Z_BUFFER = 1.75\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playermat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- table of texture URLs\nlocal nameToTexture = {\n Guardian = \"http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/\",\n Mystic = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/\",\n Neutral = \"http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/\",\n Rogue = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/\",\n Seeker = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/\",\n Survivor = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/\"\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right) - intentionally global!\nslotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variables for access\nactiveInvestigatorClass = \"Neutral\"\nactiveInvestigatorId = \"00000\"\nhasDES = false\n\nlocal isClassTextureEnabled = true\nlocal isDrawButtonVisible = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n activeInvestigatorClass = activeInvestigatorClass,\n activeInvestigatorId = activeInvestigatorId,\n isClassTextureEnabled = isClassTextureEnabled,\n isDrawButtonVisible = isDrawButtonVisible,\n playerColor = playerColor,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n activeInvestigatorClass = loadedData.activeInvestigatorClass\n activeInvestigatorId = loadedData.activeInvestigatorId\n isClassTextureEnabled = loadedData.isClassTextureEnabled\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n playerColor = loadedData.playerColor\n slotData = loadedData.slotData\n end\n\n updateMessageColor(playerColor)\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playermat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER\n\n -- 'setAsideDirection' accounts for the set aside zone being on the left or right,\n -- depending on the table position of the playermat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n\n -- Since the cast is centered on the position, shift left or right to keep\n -- the non-set aside edge of the cast at the edge of the playermat\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / scale.x\n localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n elseif tokenChecker.isChaosToken(obj) then\n -- put chaos tokens back into bag (e.g. Unrelenting)\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum\n checkForDES()\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.hasTag(\"Temporary\") == true then\n discardListOfObjects({ obj })\n elseif obj.hasTag(\"UniversalToken\") == true and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or obj.hasTag(\"DoNotReady\")) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect Forced Learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile)\n if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x \u003e -1 then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n -- discards all non-weakness and non-hidden cards from hand first\n local handCards = Player[playerColor].getHandObjects()\n local cardsToDiscard = {}\n\n for _, card in ipairs(handCards) do\n local md = JSON.decode(card.getGMNotes())\n if card.type == \"Card\" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= \"52020\") then\n table.insert(cardsToDiscard, card)\n end\n end\n\n -- perform discarding 1 by 1\n local pos = returnGlobalDiscardPosition()\n deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation())\n\n -- draw up to 5 cards\n local cardsToDraw = 5 - #handCards + #cardsToDiscard\n if cardsToDraw \u003e 0 then\n printToColor(\"Discarding \" .. #cardsToDiscard .. \" and drawing \" .. cardsToDraw .. \" card(s). (Patrice)\", messageColor)\n\n -- add some time if there are any cards to discard\n local k = 0\n if #cardsToDiscard \u003e 0 then\n k = 0.8 + (#cardsToDiscard * 0.1)\n end\n Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n local hiddenCards = {}\n local missingMetadataCards = {}\n for i, handObj in ipairs(hand) do\n if handObj.type == \"Card\" then\n -- get a name for the card or use the index if unnamed\n local name = handObj.getName()\n if name == \"\" then\n name = \"Card \" .. i\n end\n\n -- check card for metadata\n local md = JSON.decode(handObj.getGMNotes())\n if md == nil then\n table.insert(missingMetadataCards, name)\n elseif md.hidden or md.id == \"52020\" then\n table.insert(hiddenCards, name)\n else\n table.insert(choices, i)\n end\n end\n end\n\n -- print message with hidden cards\n if #hiddenCards \u003e 0 then\n local cardList = concatenateListOfStrings(hiddenCards)\n printToColor(\"Excluded (hidden): \" .. cardList, messageColor)\n end\n\n -- print message with missing metadata cards\n if #missingMetadataCards \u003e 0 then\n local cardList = concatenateListOfStrings(missingMetadataCards)\n printToColor(\"Excluded (missing data): \" .. cardList, messageColor)\n end\n\n if #choices == 0 then\n broadcastToColor(\"Didn't find any eligible cards for random discarding.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random eligible card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n broadcastToAll(getColoredName(playerColor) .. \" randomly discarded card \"\n .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\nfunction concatenateListOfStrings(list)\n local cardList\n for _, cardName in ipairs(list) do\n if not cardList then\n cardList = \"\"\n else\n cardList = cardList .. \", \"\n end\n cardList = cardList .. cardName\n end\n return cardList\nend\n\n-- checks if DES is present\nfunction checkForDES()\n hasDES = false\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.type == \"Card\" then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n\n -- position is used to exclude deck / discard\n local cardPos = self.positionToLocal(obj.getPosition())\n if cardMetadata.id == \"06159\" and cardPos.x \u003e -1 then\n hasDES = true\n break\n end\n end\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playermat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local extraToken\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n activeInvestigatorClass = notes.class\n activeInvestigatorId = notes.id\n extraToken = notes.extraToken\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n updateTexture()\n elseif activeInvestigatorId ~= \"00000\" then\n activeInvestigatorClass = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n updateTexture()\n else\n return\n end\n\n -- set proper scale for investigators\n local cardData = card.getData()\n if cardData[\"SidewaysCard\"] == true then\n -- 115% for easier readability\n card.setScale({ 1.15, 1, 1.15 })\n else\n -- Zoop-exported investigators are horizontal cards and TTS scales them differently\n card.setScale({ 0.8214, 1, 0.8214 })\n end\n\n -- remove old action tokens\n for _, obj in ipairs(searchAroundSelf(\"isUniversalToken\")) do\n obj.destruct()\n end\n\n -- spawn three regular action tokens (investigator specific one in the bottom spot)\n for i = 1, 3 do\n local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(pos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = activeInvestigatorClass })\n end)\n end\n\n -- spawn additional token (maybe specific for investigator)\n if extraToken and extraToken ~= \"None\" then\n -- local positions\n local tokenSpawnPos = {\n action = {\n Vector(-0.86, 0, -0.28), -- left of the regular three actions\n Vector(-1.54, 0, -0.28), -- right of the regular three actions\n },\n ability = {\n Vector(-1, 0, 0.118), -- bottom left corner of the investigator card\n Vector(-1, 0, -0.118), -- top left corner of the investigator card\n }\n }\n\n -- spawn tokens (split string by \"|\")\n local count = { action = 0, ability = 0 }\n for str in string.gmatch(extraToken, \"([^|]+)\") do\n local type = \"action\"\n if str == \"FreeTrigger\" or str == \"Reaction\" then\n type = \"ability\"\n end\n\n count[type] = count[type] + 1\n if count[type] \u003e 2 then\n printToColor(\"More than two extra tokens of the same type are not supported.\", playerColor)\n else\n local localSpawnPos = tokenSpawnPos[type][count[type]]\n local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(globalSpawnPos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = str })\n end)\n end\n end\n end\nend\n\n-- updates the texture of the playermat\n---@param overrideName? string Force a specific texture\nfunction updateTexture(overrideName)\n local name = \"Neutral\"\n\n -- use class specific texture if enabled\n if isClassTextureEnabled then\n name = activeInvestigatorClass\n end\n\n -- get new texture URL\n local newUrl = nameToTexture[name]\n\n -- override name if valid\n if nameToTexture[overrideName] then\n newUrl = nameToTexture[overrideName]\n end\n\n -- apply texture\n local customInfo = self.getCustomObject()\n if customInfo.image ~= newUrl then\n -- temporarily lock objects so they don't fall through the mat\n local objectsToUnlock = {}\n for _, obj in ipairs(searchAroundSelf()) do\n if not obj.getLock() then\n obj.setLock(true)\n table.insert(objectsToUnlock, obj)\n end\n end\n\n self.script_state = onSave()\n customInfo.image = newUrl\n self.setCustomObject(customInfo)\n local reloadedMat = self.reload()\n\n -- unlock objects when mat is reloaded\n Wait.condition(function()\n for _, obj in ipairs(objectsToUnlock) do\n obj.setLock(false)\n end\n end, function() return reloadedMat.loading_custom == false end)\n end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playermat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- Toggles the use of class textures\n---@param state boolean Whether the class texture should be used or not\nfunction useClassTexture(state)\n if state == isClassTextureEnabled then return end\n isClassTextureEnabled = state\n updateTexture()\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\n\n-- returns the colored steam name or color\nfunction getColoredName(playerColor)\n local displayName = playerColor\n if Player[playerColor].steam_name then\n displayName = Player[playerColor].steam_name\n end\n\n -- add bb-code\n return \"[\" .. Color.fromString(playerColor):toHex() .. \"]\" .. displayName .. \"[-]\"\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playermat\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"activeInvestigatorClass\":\"Neutral\",\"activeInvestigatorId\":\"00000\",\"isClassTextureEnabled\":true,\"isDrawButtonVisible\":false,\"playerColor\":\"Green\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", "MeasureMovement": false, "Memo": "Green", "Name": "Custom_Tile", @@ -51438,47 +31690,57 @@ "z": 0.118 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -0.865, + "x": -0.86, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1, + "x": -1.03, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.18, + "x": -1.2, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" ] }, { "Position": { - "x": -1.36, + "x": -1.37, "y": 0.1, "z": -0.28 }, "Tags": [ - "ActionToken" + "UniversalToken" + ] + }, + { + "Position": { + "x": -1.54, + "y": 0.1, + "z": -0.28 + }, + "Tags": [ + "UniversalToken" ] }, { @@ -51769,8 +32031,8 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playmat\")\nend)\n__bundle_register(\"playermat/Playmat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playmat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right)\nlocal slotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variable so it can be reset by the Clean Up Helper\nactiveInvestigatorId = \"00000\"\nlocal isDrawButtonVisible = false\n\n-- global variable to report \"Dream-Enhancing Serum\" status\nisDES = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n playerColor = playerColor,\n activeInvestigatorId = activeInvestigatorId,\n isDrawButtonVisible = isDrawButtonVisible,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n playerColor = loadedData.playerColor\n activeInvestigatorId = loadedData.activeInvestigatorId\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n slotData = loadedData.slotData\n end\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playmat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local bounds = self.getBoundsNormalized()\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n -- Since the cast is centered on the position, shift left or right to keep the non-set aside edge\n -- of the cast at the edge of the playmat\n -- setAsideDirection accounts for the set aside zone being on the left or right, depending on the\n -- table position of the playmat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / self.getScale().x\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n\n -- put chaos tokens back into bag (e.g. Unrelenting)\n elseif tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find forcedLearning\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.getDescription() == \"Action Token\" and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or false) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect forced learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards\n if cardMetadata.uses ~= nil then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n local handSize = #Player[playerColor].getHandObjects()\n if handSize \u003c 5 then\n local cardsToDraw = 5 - handSize\n printToColor(\"Drawing \" .. cardsToDraw .. \" cards (Patrice)\", messageColor)\n drawCardsWithReshuffle(cardsToDraw)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n for i = 1, #hand do\n local notes = JSON.decode(hand[i].getGMNotes())\n if notes ~= nil then\n if notes.hidden ~= true then\n table.insert(choices, i)\n end\n else\n table.insert(choices, i)\n end\n end\n\n if #choices == 0 then\n broadcastToColor(\"Hidden cards can't be randomly discarded.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random non-hidden card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n\n local playerName = Player[playerColor].steam_name or playerColor\n broadcastToAll(playerName .. \" randomly discarded card \" .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playmat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n -- detect if \"Dream-Enhancing Serum\" is placed\n if object.getName() == \"Dream-Enhancing Serum\" then isDES = true end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- detect if \"Dream-Enhancing Serum\" is removed\nfunction onCollisionExit(collisionInfo)\n if collisionInfo.collision_object.getName() == \"Dream-Enhancing Serum\" then isDES = false end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenManager.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false and\n obj.getDescription() ~= \"Action Token\" then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local class\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n class = notes.class\n activeInvestigatorId = notes.id\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n elseif activeInvestigatorId ~= \"00000\" then\n class = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n return\n end\n\n -- change state of action tokens\n local search = searchArea(self.positionToWorld({ -1.1, 0.05, -0.27 }), { 4, 1, 1 })\n local smallToken = nil\n local STATE_TABLE = {\n [\"Guardian\"] = 1,\n [\"Seeker\"] = 2,\n [\"Rogue\"] = 3,\n [\"Mystic\"] = 4,\n [\"Survivor\"] = 5,\n [\"Neutral\"] = 6\n }\n\n for _, obj in ipairs(search) do\n if obj.getDescription() == \"Action Token\" and obj.getStateId() \u003e 0 then\n if obj.getScale().x \u003c 0.4 then\n smallToken = obj\n else\n setObjectState(obj, STATE_TABLE[class])\n end\n end\n end\n\n -- update the small token with special action for certain investigators\n local SPECIAL_ACTIONS = {\n [\"04002\"] = 8, -- Ursula Downs\n [\"01002\"] = 9, -- Daisy Walker\n [\"01502\"] = 9, -- Daisy Walker\n [\"01002-pb\"] = 9, -- Daisy Walker\n [\"06003\"] = 10, -- Tony Morgan\n [\"04003\"] = 11, -- Finn Edwards\n [\"08016\"] = 14 -- Bob Jenkins\n }\n\n if smallToken ~= nil then\n setObjectState(smallToken, SPECIAL_ACTIONS[activeInvestigatorId] or STATE_TABLE[class])\n end\nend\n\nfunction setObjectState(obj, stateId)\n if obj.getStateId() ~= stateId then obj.setState(stateId) end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playmat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playmat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "{\"activeInvestigatorId\":\"00000\",\"isDrawButtonVisible\":false,\"playerColor\":\"Red\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/Playermat\")\nend)\n__bundle_register(\"playermat/Playermat\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\nlocal tokenManager = require(\"core/token/TokenManager\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n-- we use this to turn off collision handling until onLoad() is complete\nlocal collisionEnabled = false\nlocal currentlyEditingSlots = false\n\n-- x-Values for discard buttons\nlocal DISCARD_BUTTON_X_START = -1.365\nlocal DISCARD_BUTTON_X_OFFSET = 0.455\n\nlocal SEARCH_AROUND_SELF_X_BUFFER = 8\nlocal SEARCH_AROUND_SELF_Z_BUFFER = 1.75\n\n-- defined areas for object searching\nlocal MAIN_PLAY_AREA = {\n upperLeft = { x = 1.98, z = 0.736 },\n lowerRight = { x = -0.79, z = -0.39 }\n}\nlocal INVESTIGATOR_AREA = {\n upperLeft = { x = -1.084, z = 0.06517 },\n lowerRight = { x = -1.258, z = -0.0805 }\n}\nlocal THREAT_AREA = {\n upperLeft = { x = 1.53, z = -0.34 },\n lowerRight = { x = -1.13, z = -0.92 }\n}\nlocal DECK_DISCARD_AREA = {\n upperLeft = { x = -1.62, z = 0.855 },\n lowerRight = { x = -2.02, z = -0.245 },\n center = { x = -1.82, y = 0.5, z = 0.305 },\n size = { x = 0.4, y = 3, z = 1.1 }\n}\n\n-- local positions\nlocal DRAW_DECK_POSITION = { x = -1.82, y = 0.1, z = 0 }\nlocal DISCARD_PILE_POSITION = { x = -1.82, y = 0.1, z = 0.61 }\nlocal DRAWN_ENCOUNTER_POSITION = { x = 1.365, y = 0.5, z = -0.625 }\n\n-- global position of encounter discard pile\nlocal ENCOUNTER_DISCARD_POSITION = { x = -3.85, y = 1.5, z = 10.38 }\n\n-- used for the buttons on the right side of the playermat\n-- starts off with the data for the \"Upkeep\" button and will then be changed\nlocal buttonParameters = {\n label = \"Upkeep\",\n click_function = \"doUpkeep\",\n tooltip = \"Right-click to change color\",\n function_owner = self,\n position = { x = 1.82, y = 0.1, z = -0.45 },\n scale = { 0.12, 0.12, 0.12 },\n width = 1000,\n height = 280,\n font_size = 180\n}\n\n-- table of texture URLs\nlocal nameToTexture = {\n Guardian = \"http://cloud-3.steamusercontent.com/ugc/2501268517241599869/179119CA88170D9F5C87CD00D267E6F9F397D2F7/\",\n Mystic = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600113/F6473F92B3435C32A685BB4DC2A88C2504DDAC4F/\",\n Neutral = \"http://cloud-3.steamusercontent.com/ugc/2462982115659543571/5D778EA4BC682DAE97E8F59A991BCF8CB3979B04/\",\n Rogue = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600395/00CFAFC13D7B6EACC147D22A40AF9FBBFFAF3136/\",\n Seeker = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600579/92DEB412D8D3A9C26D1795CEA0335480409C3E4B/\",\n Survivor = \"http://cloud-3.steamusercontent.com/ugc/2501268517241600848/CEB685E9C8A4A3C18A4B677A519B49423B54E886/\"\n}\n\n-- translation table for slot names to characters for special font\nlocal slotNameToChar = {\n [\"any\"] = \"\",\n [\"Accessory\"] = \"C\",\n [\"Ally\"] = \"E\",\n [\"Arcane\"] = \"G\",\n [\"Body\"] = \"K\",\n [\"Hand (right)\"] = \"M\",\n [\"Hand (left)\"] = \"M\",\n [\"Hand x2\"] = \"N\",\n [\"Tarot\"] = \"A\"\n}\n\n-- slot symbol for the respective slot (from top left to bottom right) - intentionally global!\nslotData = {}\nlocal defaultSlotData = {\n -- 1st row\n \"any\", \"any\", \"any\", \"Tarot\", \"Hand (left)\", \"Hand (right)\", \"Ally\",\n\n -- 2nd row\n \"any\", \"any\", \"any\", \"Accessory\", \"Arcane\", \"Arcane\", \"Body\"\n}\n\n-- global variables for access\nactiveInvestigatorClass = \"Neutral\"\nactiveInvestigatorId = \"00000\"\nhasDES = false\n\nlocal isClassTextureEnabled = true\nlocal isDrawButtonVisible = false\n\n-- table of type-object reference pairs of all owned objects\nlocal ownedObjects = {}\nlocal matColor = self.getMemo()\n\nfunction onSave()\n return JSON.encode({\n activeInvestigatorClass = activeInvestigatorClass,\n activeInvestigatorId = activeInvestigatorId,\n isClassTextureEnabled = isClassTextureEnabled,\n isDrawButtonVisible = isDrawButtonVisible,\n playerColor = playerColor,\n slotData = slotData\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n activeInvestigatorClass = loadedData.activeInvestigatorClass\n activeInvestigatorId = loadedData.activeInvestigatorId\n isClassTextureEnabled = loadedData.isClassTextureEnabled\n isDrawButtonVisible = loadedData.isDrawButtonVisible\n playerColor = loadedData.playerColor\n slotData = loadedData.slotData\n end\n\n updateMessageColor(playerColor)\n\n self.interactable = false\n\n -- get object references to owned objects\n ownedObjects = guidReferenceApi.getObjectsByOwner(matColor)\n\n -- discard button creation\n for i = 1, 6 do\n makeDiscardButton(i)\n end\n\n self.createButton({\n click_function = \"drawEncounterCard\",\n function_owner = self,\n position = { -1.84, 0, -0.65 },\n rotation = { 0, 80, 0 },\n width = 265,\n height = 190\n })\n\n self.createButton({\n click_function = \"drawChaosTokenButton\",\n function_owner = self,\n position = { 1.85, 0, -0.74 },\n rotation = { 0, -45, 0 },\n width = 135,\n height = 135\n })\n\n -- Upkeep button: can use the default parameters for this\n self.createButton(buttonParameters)\n\n -- Slot editing button: modified default data\n buttonParameters.label = \"Edit Slots\"\n buttonParameters.click_function = \"toggleSlotEditing\"\n buttonParameters.tooltip = \"Right-click to reset slot symbols\"\n buttonParameters.position.z = 0.92\n self.createButton(buttonParameters)\n\n showDrawButton(isDrawButtonVisible)\n redrawSlotSymbols()\n math.randomseed(os.time())\n Wait.time(function() collisionEnabled = true end, 0.1)\nend\n\n---------------------------------------------------------\n-- utility functions\n---------------------------------------------------------\n\n-- searches an area and optionally filters the result\nfunction searchArea(origin, size, filter)\n return searchLib.inArea(origin, self.getRotation(), size, filter)\nend\n\n-- finds all objects on the playermat and associated set aside zone.\nfunction searchAroundSelf(filter)\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n -- Increase the width to cover the set aside zone\n bounds.size.x = bounds.size.x + SEARCH_AROUND_SELF_X_BUFFER\n bounds.size.y = 1\n bounds.size.z = bounds.size.z + SEARCH_AROUND_SELF_Z_BUFFER\n\n -- 'setAsideDirection' accounts for the set aside zone being on the left or right,\n -- depending on the table position of the playermat\n local setAsideDirection = bounds.center.z \u003e 0 and 1 or -1\n\n -- Since the cast is centered on the position, shift left or right to keep\n -- the non-set aside edge of the cast at the edge of the playermat\n local localCenter = self.positionToLocal(bounds.center)\n localCenter.x = localCenter.x + setAsideDirection * SEARCH_AROUND_SELF_X_BUFFER / 2 / scale.x\n localCenter.z = localCenter.z - SEARCH_AROUND_SELF_Z_BUFFER / 2 / scale.z\n return searchArea(self.positionToWorld(localCenter), bounds.size, filter)\nend\n\n-- searches the area around the draw deck and discard pile\nfunction searchDeckAndDiscardArea(filter)\n local pos = self.positionToWorld(DECK_DISCARD_AREA.center)\n local scale = self.getScale()\n local size = {\n x = DECK_DISCARD_AREA.size.x * scale.x,\n y = DECK_DISCARD_AREA.size.y,\n z = DECK_DISCARD_AREA.size.z * scale.z\n }\n return searchArea(pos, size, filter)\nend\n\n-- rounds a number to the specified amount of decimal places\n---@param num number Initial value\n---@param numDecimalPlaces number Amount of decimal places\n---@return number: rounded number\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n-- edits the label of a button\n---@param oldLabel string Old label of the button\n---@param newLabel string New label of the button\nfunction editButtonLabel(oldLabel, newLabel)\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == oldLabel then\n self.editButton({ index = buttons[i].index, label = newLabel })\n end\n end\nend\n\n-- updates the internal \"messageColor\" which is used for print/broadcast statements if no player is seated\n---@param clickedByColor string Colorstring of player who clicked a button\nfunction updateMessageColor(clickedByColor)\n messageColor = Player[playerColor].seated and playerColor or clickedByColor\nend\n\n---------------------------------------------------------\n-- Discard buttons\n---------------------------------------------------------\n\n-- handles discarding for a list of objects\n---@param objList table List of objects to discard\nfunction discardListOfObjects(objList)\n for _, obj in ipairs(objList) do\n if obj.type == \"Card\" or obj.type == \"Deck\" then\n if obj.hasTag(\"PlayerCard\") then\n deckLib.placeOrMergeIntoDeck(obj, returnGlobalDiscardPosition(), self.getRotation())\n else\n deckLib.placeOrMergeIntoDeck(obj, ENCOUNTER_DISCARD_POSITION, { x = 0, y = -90, z = 0 })\n end\n elseif tokenChecker.isChaosToken(obj) then\n -- put chaos tokens back into bag (e.g. Unrelenting)\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif not obj.getLock() and not obj.hasTag(\"DontDiscard\") then\n -- don't touch locked objects (like the table etc.) or specific objects (like key tokens)\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n-- build a discard button to discard from searchPosition\n---@param id number Index of the discard button (from left to right, must be unique)\nfunction makeDiscardButton(id)\n local xValue = DISCARD_BUTTON_X_START + (id - 1) * DISCARD_BUTTON_X_OFFSET\n local position = { xValue, 0.1, -0.94 }\n local searchPosition = { -position[1], position[2], position[3] + 0.32 }\n local handlerName = 'handler' .. id\n self.setVar(handlerName, function()\n local cardSizeSearch = { 2, 1, 3.2 }\n local globalSearchPosition = self.positionToWorld(searchPosition)\n local searchResult = searchArea(globalSearchPosition, cardSizeSearch)\n return discardListOfObjects(searchResult)\n end)\n self.createButton({\n label = \"Discard\",\n click_function = handlerName,\n function_owner = self,\n position = position,\n scale = { 0.12, 0.12, 0.12 },\n width = 900,\n height = 350,\n font_size = 220\n })\nend\n\n---------------------------------------------------------\n-- Upkeep button\n---------------------------------------------------------\n\n-- calls the Upkeep function with correct parameter\nfunction doUpkeepFromHotkey(clickedByColor)\n doUpkeep(_, clickedByColor)\nend\n\nfunction doUpkeep(_, clickedByColor, isRightClick)\n if isRightClick then\n changeColor(clickedByColor)\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- unexhaust cards in play zone, flip action tokens and find Forced Learning / Dream-Enhancing Serum\n checkForDES()\n local forcedLearning = false\n local rot = self.getRotation()\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.hasTag(\"Temporary\") == true then\n discardListOfObjects({ obj })\n elseif obj.hasTag(\"UniversalToken\") == true and obj.is_face_down then\n obj.flip()\n elseif obj.type == \"Card\" and not inArea(self.positionToLocal(obj.getPosition()), INVESTIGATOR_AREA) then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n if not (obj.getVar(\"do_not_ready\") or obj.hasTag(\"DoNotReady\")) then\n local cardRotation = round(obj.getRotation().y, 0) - rot.y\n local yRotDiff = 0\n\n if cardRotation \u003c 0 then\n cardRotation = cardRotation + 360\n end\n\n -- rotate cards to the next multiple of 90° towards 0°\n if cardRotation \u003e 90 and cardRotation \u003c= 180 then\n yRotDiff = 90\n elseif cardRotation \u003c 270 and cardRotation \u003e 180 then\n yRotDiff = 270\n end\n\n -- set correct rotation for face-down cards\n rot.z = obj.is_face_down and 180 or 0\n obj.setRotation({ rot.x, rot.y + yRotDiff, rot.z })\n end\n\n -- detect Forced Learning to handle card drawing accordingly\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n\n -- maybe replenish uses on certain cards (don't continue for cards on the deck (Norman) or in the discard pile)\n if cardMetadata.uses ~= nil and self.positionToLocal(obj.getPosition()).x \u003e -1 then\n tokenManager.maybeReplenishCard(obj, cardMetadata.uses, self)\n end\n elseif obj.type == \"Deck\" and forcedLearning == false then\n -- check decks for forced learning\n for _, deepObj in ipairs(obj.getObjects()) do\n local cardMetadata = JSON.decode(deepObj.gm_notes) or {}\n if cardMetadata.id == \"08031\" then\n forcedLearning = true\n end\n end\n end\n end\n\n -- flip investigator mini-card and summoned servitor mini-card\n -- (all characters allowed to account for custom IDs - e.g. 'Z0000' for TTS Zoop generated IDs)\n local miniId = string.match(activeInvestigatorId, \".....\") .. \"-m\"\n for _, obj in ipairs(getObjects()) do\n if obj.type == \"Card\" and obj.is_face_down then\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.type == \"Minicard\" and (notes.id == miniId or notes.id == \"09080-m\") then\n obj.flip()\n end\n end\n end\n\n -- gain a resource (or two if playing Jenny Barnes)\n if string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"02003\" then\n updateCounter({ type = \"ResourceCounter\", modifier = 2 })\n printToColor(\"Gaining 2 resources (Jenny)\", messageColor)\n else\n updateCounter({ type = \"ResourceCounter\", modifier = 1 })\n end\n\n -- draw a card (with handling for Patrice and Forced Learning)\n if activeInvestigatorId == \"06005\" then\n if forcedLearning then\n printToColor(\"Wow, did you really take 'Versatile' to play Patrice with 'Forced Learning'?\"\n .. \" Choose which draw replacement effect takes priority and draw cards accordingly.\", messageColor)\n else\n -- discards all non-weakness and non-hidden cards from hand first\n local handCards = Player[playerColor].getHandObjects()\n local cardsToDiscard = {}\n\n for _, card in ipairs(handCards) do\n local md = JSON.decode(card.getGMNotes())\n if card.type == \"Card\" and md ~= nil and (not md.weakness and not md.hidden and md.id ~= \"52020\") then\n table.insert(cardsToDiscard, card)\n end\n end\n\n -- perform discarding 1 by 1\n local pos = returnGlobalDiscardPosition()\n deckLib.placeOrMergeIntoDeck(cardsToDiscard, pos, self.getRotation())\n\n -- draw up to 5 cards\n local cardsToDraw = 5 - #handCards + #cardsToDiscard\n if cardsToDraw \u003e 0 then\n printToColor(\"Discarding \" .. #cardsToDiscard .. \" and drawing \" .. cardsToDraw .. \" card(s). (Patrice)\", messageColor)\n\n -- add some time if there are any cards to discard\n local k = 0\n if #cardsToDiscard \u003e 0 then\n k = 0.8 + (#cardsToDiscard * 0.1)\n end\n Wait.time(function() drawCardsWithReshuffle(cardsToDraw) end, k)\n end\n end\n elseif forcedLearning then\n printToColor(\"Drawing 2 cards, discard 1 (Forced Learning)\", messageColor)\n drawCardsWithReshuffle(2)\n elseif activeInvestigatorId == \"89001\" then\n printToColor(\"Drawing 2 cards (Subject 5U-21)\", messageColor)\n drawCardsWithReshuffle(2)\n else\n drawCardsWithReshuffle(1)\n end\nend\n\n-- click function for \"draw 1 button\" (that can be added via option panel)\nfunction doDrawOne(_, clickedByColor)\n updateMessageColor(clickedByColor)\n drawCardsWithReshuffle(1)\nend\n\n-- draws the specified amount of cards (and shuffles the discard if necessary)\n---@param numCards number Number of cards to draw\nfunction drawCardsWithReshuffle(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n\n -- Norman Withers handling\n local harbinger = false\n if deckAreaObjects.topCard and deckAreaObjects.topCard.getName() == \"The Harbinger\" then\n harbinger = true\n elseif deckAreaObjects.draw and not deckAreaObjects.draw.is_face_down then\n local cards = deckAreaObjects.draw.getObjects()\n if cards[#cards].name == \"The Harbinger\" then\n harbinger = true\n end\n end\n\n if harbinger then\n printToColor(\"The Harbinger is on top of your deck, not drawing cards\", messageColor)\n return\n end\n\n local topCardDetected = false\n if deckAreaObjects.topCard ~= nil then\n deckAreaObjects.topCard.deal(1, playerColor)\n topCardDetected = true\n numCards = numCards - 1\n if numCards == 0 then\n flipTopCardFromDeck()\n return\n end\n end\n\n local deckSize = 1\n if deckAreaObjects.draw == nil then\n deckSize = 0\n elseif deckAreaObjects.draw.type == \"Deck\" then\n deckSize = #deckAreaObjects.draw.getObjects()\n end\n\n if deckSize \u003e= numCards then\n drawCards(numCards)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n else\n drawCards(deckSize)\n if deckAreaObjects.discard ~= nil then\n shuffleDiscardIntoDeck()\n Wait.time(function()\n drawCards(numCards - deckSize)\n -- flip top card again for Norman\n if topCardDetected and string.match(activeInvestigatorId, \"%d%d%d%d%d\") == \"08004\" then\n flipTopCardFromDeck()\n end\n end, 1)\n end\n printToColor(\"Take 1 horror (drawing card from empty deck)\", messageColor)\n end\nend\n\n-- get the draw deck and discard pile objects and returns the references\n---@return table: string-indexed table with references to the found objects\nfunction getDeckAreaObjects()\n local deckAreaObjects = {}\n for _, object in ipairs(searchDeckAndDiscardArea(\"isCardOrDeck\")) do\n if self.positionToLocal(object.getPosition()).z \u003e 0.5 then\n deckAreaObjects.discard = object\n -- Norman Withers handling\n elseif object.type == \"Card\" and not object.is_face_down then\n deckAreaObjects.topCard = object\n else\n deckAreaObjects.draw = object\n end\n end\n return deckAreaObjects\nend\n\n-- draws the specified number of cards (reshuffling of discard pile is handled separately)\n---@param numCards number Number of cards to draw\nfunction drawCards(numCards)\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.draw then\n deckAreaObjects.draw.deal(numCards, playerColor)\n end\nend\n\nfunction shuffleDiscardIntoDeck()\n local deckAreaObjects = getDeckAreaObjects()\n if not deckAreaObjects.discard.is_face_down then\n deckAreaObjects.discard.flip()\n end\n deckAreaObjects.discard.shuffle()\n deckAreaObjects.discard.setPositionSmooth(self.positionToWorld(DRAW_DECK_POSITION), false, false)\nend\n\n-- utility function for Norman Withers to flip the top card to the revealed side\nfunction flipTopCardFromDeck()\n Wait.time(function()\n local deckAreaObjects = getDeckAreaObjects()\n if deckAreaObjects.topCard then\n elseif deckAreaObjects.draw then\n if deckAreaObjects.draw.type == \"Card\" then\n deckAreaObjects.draw.flip()\n else\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n local pos = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n deckAreaObjects.draw.takeObject({ position = pos, flip = true })\n end\n end\n end, 0.1)\nend\n\n-- discard a random non-hidden card from hand\nfunction doDiscardOne()\n local hand = Player[playerColor].getHandObjects()\n if #hand == 0 then\n broadcastToColor(\"Cannot discard from empty hand!\", messageColor, \"Red\")\n else\n local choices = {}\n local hiddenCards = {}\n local missingMetadataCards = {}\n for i, handObj in ipairs(hand) do\n if handObj.type == \"Card\" then\n -- get a name for the card or use the index if unnamed\n local name = handObj.getName()\n if name == \"\" then\n name = \"Card \" .. i\n end\n\n -- check card for metadata\n local md = JSON.decode(handObj.getGMNotes())\n if md == nil then\n table.insert(missingMetadataCards, name)\n elseif md.hidden or md.id == \"52020\" then\n table.insert(hiddenCards, name)\n else\n table.insert(choices, i)\n end\n end\n end\n\n -- print message with hidden cards\n if #hiddenCards \u003e 0 then\n local cardList = concatenateListOfStrings(hiddenCards)\n printToColor(\"Excluded (hidden): \" .. cardList, messageColor)\n end\n\n -- print message with missing metadata cards\n if #missingMetadataCards \u003e 0 then\n local cardList = concatenateListOfStrings(missingMetadataCards)\n printToColor(\"Excluded (missing data): \" .. cardList, messageColor)\n end\n\n if #choices == 0 then\n broadcastToColor(\"Didn't find any eligible cards for random discarding.\", messageColor, \"Orange\")\n return\n end\n\n -- get a random eligible card (from the \"choices\" table)\n local num = math.random(1, #choices)\n deckLib.placeOrMergeIntoDeck(hand[choices[num]], returnGlobalDiscardPosition(), self.getRotation())\n broadcastToAll(getColoredName(playerColor) .. \" randomly discarded card \"\n .. choices[num] .. \"/\" .. #hand .. \".\", \"White\")\n end\nend\n\nfunction concatenateListOfStrings(list)\n local cardList\n for _, cardName in ipairs(list) do\n if not cardList then\n cardList = \"\"\n else\n cardList = cardList .. \", \"\n end\n cardList = cardList .. cardName\n end\n return cardList\nend\n\n-- checks if DES is present\nfunction checkForDES()\n hasDES = false\n for _, obj in ipairs(searchAroundSelf()) do\n if obj.type == \"Card\" then\n local cardMetadata = JSON.decode(obj.getGMNotes()) or {}\n\n -- position is used to exclude deck / discard\n local cardPos = self.positionToLocal(obj.getPosition())\n if cardMetadata.id == \"06159\" and cardPos.x \u003e -1 then\n hasDES = true\n break\n end\n end\n end\nend\n\n---------------------------------------------------------\n-- slot symbol displaying\n---------------------------------------------------------\n\n-- this will redraw the XML for the slot symbols based on the slotData table\nfunction redrawSlotSymbols()\n local xml = {}\n local snapId = 0\n\n -- use the snap point positions in the main play area for positions\n for _, snap in ipairs(self.getSnapPoints()) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n snapId = snapId + 1\n local slotName = slotData[snapId]\n\n -- conversion from regular coordinates to XML\n local x = snap.position.x * 100\n local y = snap.position.z * 100\n\n -- XML for a single slot (panel with text in the special font)\n local slotXML = {\n tag = \"Panel\",\n attributes = {\n id = \"slotPanel\" .. snapId,\n scale = \"0.1 0.1 1\",\n width = \"175\",\n height = \"175\",\n position = x .. \" \" .. y .. \" -11\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n id = \"slot\" .. snapId,\n rotation = getSlotRotation(slotName),\n fontSize = \"145\",\n font = \"font_arkhamicons\",\n color = \"#414141CB\",\n text = slotNameToChar[slotName]\n }\n }\n }\n }\n table.insert(xml, slotXML)\n end\n end\n\n self.UI.setXmlTable(xml)\nend\n\n-- toggle the \"slot editing mode\"\nfunction toggleSlotEditing(_, clickedByColor, isRightClick)\n if isRightClick then\n resetSlotSymbols()\n return\n end\n\n updateMessageColor(clickedByColor)\n\n -- toggle internal variable\n currentlyEditingSlots = not currentlyEditingSlots\n\n if currentlyEditingSlots then\n editButtonLabel(\"Edit Slots\", \"Stop editing\")\n broadcastToColor(\"Click on a slot symbol (or an empty slot) to edit it.\", messageColor, \"Orange\")\n addClickFunctionToSlots()\n else\n editButtonLabel(\"Stop editing\", \"Edit Slots\")\n redrawSlotSymbols()\n end\nend\n\n-- click function for slot symbols during the \"slot editing mode\"\nfunction slotClickfunction(player, _, id)\n local slotIndex = id:gsub(\"slotPanel\", \"\")\n slotIndex = tonumber(slotIndex)\n\n -- make a list of the table keys as options for the dialog box\n local slotNames = {}\n for slotName, _ in pairs(slotNameToChar) do\n table.insert(slotNames, slotName)\n end\n\n -- prompt player to choose symbol\n player.showOptionsDialog(\"Choose Slot Symbol\", slotNames, slotData[slotIndex],\n function(chosenSlotName)\n slotData[slotIndex] = chosenSlotName\n\n -- update slot symbol\n self.UI.setAttribute(\"slot\" .. slotIndex, \"text\", slotNameToChar[chosenSlotName])\n\n -- update slot rotation\n self.UI.setAttribute(\"slot\" .. slotIndex, \"rotation\", getSlotRotation(chosenSlotName))\n end\n )\nend\n\n-- helper function to rotate the left hand\nfunction getSlotRotation(slotName)\n if slotName == \"Hand (left)\" then\n return \"0 180 180\"\n else\n return \"0 0 180\"\n end\nend\n\n-- reset the slot symbols by making a deep copy of the default data and redrawing\nfunction resetSlotSymbols()\n slotData = {}\n for _, slotName in ipairs(defaultSlotData) do\n table.insert(slotData, slotName)\n end\n\n redrawSlotSymbols()\n\n -- need to re-add the click functions if currently in edit mode\n if currentlyEditingSlots then\n addClickFunctionToSlots()\n end\nend\n\n-- enables the click functions for editing\nfunction addClickFunctionToSlots()\n for i = 1, #slotData do\n self.UI.setAttribute(\"slotPanel\" .. i, \"onClick\", \"slotClickfunction\")\n end\nend\n\n---------------------------------------------------------\n-- color related functions\n---------------------------------------------------------\n\n-- changes the player color\nfunction changeColor(clickedByColor)\n local colorList = Player.getColors()\n\n -- remove existing colors from the list of choices\n for _, existingColor in ipairs(Player.getAvailableColors()) do\n for i, newColor in ipairs(colorList) do\n if existingColor == newColor or newColor == \"Black\" or newColor == \"Grey\" then\n table.remove(colorList, i)\n end\n end\n end\n\n -- show the option dialog for color selection to the player that triggered this\n Player[clickedByColor].showOptionsDialog(\"Select a new color:\", colorList, _, function(color)\n -- update the color of the hand zone\n local handZone = ownedObjects.HandZone\n handZone.setValue(color)\n\n -- if the seated player clicked this, reseat him to the new color\n if clickedByColor == playerColor then\n navigationOverlayApi.copyVisibility(playerColor, color)\n Player[playerColor].changeColor(color)\n end\n\n -- update the internal variable\n playerColor = color\n end)\nend\n\n---------------------------------------------------------\n-- playermat token spawning\n---------------------------------------------------------\n\n-- Finds all customizable cards in this play area and updates their metadata based on the selections\n-- on the matching upgrade sheet.\n-- This method is theoretically O(n^2), and should be used sparingly. In practice it will only be\n-- called when a checkbox is added or removed in-game (which should be rare), and is bounded by the\n-- number of customizable cards in play.\nfunction syncAllCustomizableCards()\n for _, card in ipairs(searchAroundSelf(\"isCard\")) do\n syncCustomizableMetadata(card)\n end\nend\n\nfunction syncCustomizableMetadata(card)\n local cardMetadata = JSON.decode(card.getGMNotes()) or {}\n if cardMetadata == nil or cardMetadata.customizations == nil then return end\n\n for _, upgradeSheet in ipairs(searchAroundSelf(\"isCard\")) do\n local upgradeSheetMetadata = JSON.decode(upgradeSheet.getGMNotes()) or {}\n if upgradeSheetMetadata.id == (cardMetadata.id .. \"-c\") then\n for i, customization in ipairs(cardMetadata.customizations) do\n if customization.replaces ~= nil and customization.replaces.uses ~= nil then\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n cardMetadata.uses = customization.replaces.uses\n card.setGMNotes(JSON.encode(cardMetadata))\n else\n -- TODO: Get the original metadata to restore it... maybe. This should only be\n -- necessary in the very unlikely case that a user un-checks a previously-full upgrade\n -- row while the card is in play. It will be much easier once the AllPlayerCardsApi is\n -- in place, so defer until it is\n end\n end\n end\n end\n end\nend\n\nfunction spawnTokensFor(object)\n local extraUses = {}\n if activeInvestigatorId == \"03004\" then\n extraUses[\"Charge\"] = 1\n end\n\n tokenManager.spawnForCard(object, extraUses)\nend\n\nfunction onCollisionEnter(collisionInfo)\n local object = collisionInfo.collision_object\n\n -- only continue if loading is completed\n if not collisionEnabled then return end\n\n -- only continue for cards\n if object.type ~= \"Card\" then return end\n\n maybeUpdateActiveInvestigator(object)\n syncCustomizableMetadata(object)\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n elseif shouldSpawnTokens(object) then\n spawnTokensFor(object)\n end\nend\n\n-- checks if tokens should be spawned for the provided card\nfunction shouldSpawnTokens(card)\n if card.is_face_down then\n return false\n end\n\n local localCardPos = self.positionToLocal(card.getPosition())\n local metadata = JSON.decode(card.getGMNotes())\n\n -- If no metadata we don't know the type, so only spawn in the main area\n if metadata == nil then\n return inArea(localCardPos, MAIN_PLAY_AREA)\n end\n\n -- Spawn tokens for assets and events on the main area\n if inArea(localCardPos, MAIN_PLAY_AREA)\n and (metadata.type == \"Asset\"\n or metadata.type == \"Event\") then\n return true\n end\n\n -- Spawn tokens for all encounter types in the threat area\n if inArea(localCardPos, THREAT_AREA)\n and (metadata.type == \"Treachery\"\n or metadata.type == \"Enemy\"\n or metadata.weakness) then\n return true\n end\n\n return false\nend\n\nfunction onObjectEnterContainer(container, object)\n if object.type ~= \"Card\" then return end\n\n local localCardPos = self.positionToLocal(object.getPosition())\n if inArea(localCardPos, DECK_DISCARD_AREA) then\n tokenSpawnTrackerApi.resetTokensSpawned(object)\n removeTokensFromObject(object)\n end\nend\n\n-- removes tokens from the provided card/deck\nfunction removeTokensFromObject(object)\n if object.hasTag(\"CardThatSeals\") then\n local func = object.getVar(\"resetSealedTokens\") -- check if function exists (it won't for older custom content)\n if func ~= nil then\n object.call(\"resetSealedTokens\")\n end\n end\n\n for _, obj in ipairs(searchLib.onObject(object)) do\n if tokenChecker.isChaosToken(obj) then\n chaosBagApi.returnChaosTokenToBag(obj, false)\n elseif obj.getGUID() ~= \"4ee1f2\" and -- table\n obj ~= self and\n obj.type ~= \"Deck\" and\n obj.type ~= \"Card\" and\n obj.memo ~= nil and\n obj.getLock() == false then\n ownedObjects.Trash.putObject(obj)\n end\n end\nend\n\n---------------------------------------------------------\n-- investigator ID grabbing and skill tracker\n---------------------------------------------------------\n\n-- updates the internal investigator id and action tokens if an investigator card is detected\n---@param card tts__Object Card that might be an investigator\nfunction maybeUpdateActiveInvestigator(card)\n if not inArea(self.positionToLocal(card.getPosition()), INVESTIGATOR_AREA) then return end\n\n local notes = JSON.decode(card.getGMNotes())\n local extraToken\n\n if notes ~= nil and notes.type == \"Investigator\" and notes.id ~= nil then\n if notes.id == activeInvestigatorId then return end\n activeInvestigatorClass = notes.class\n activeInvestigatorId = notes.id\n extraToken = notes.extraToken\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", {\n notes.willpowerIcons,\n notes.intellectIcons,\n notes.combatIcons,\n notes.agilityIcons\n })\n updateTexture()\n elseif activeInvestigatorId ~= \"00000\" then\n activeInvestigatorClass = \"Neutral\"\n activeInvestigatorId = \"00000\"\n ownedObjects.InvestigatorSkillTracker.call(\"updateStats\", { 1, 1, 1, 1 })\n updateTexture()\n else\n return\n end\n\n -- set proper scale for investigators\n local cardData = card.getData()\n if cardData[\"SidewaysCard\"] == true then\n -- 115% for easier readability\n card.setScale({ 1.15, 1, 1.15 })\n else\n -- Zoop-exported investigators are horizontal cards and TTS scales them differently\n card.setScale({ 0.8214, 1, 0.8214 })\n end\n\n -- remove old action tokens\n for _, obj in ipairs(searchAroundSelf(\"isUniversalToken\")) do\n obj.destruct()\n end\n\n -- spawn three regular action tokens (investigator specific one in the bottom spot)\n for i = 1, 3 do\n local pos = self.positionToWorld(Vector(-1.54 + i * 0.17, 0, -0.28)):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(pos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = activeInvestigatorClass })\n end)\n end\n\n -- spawn additional token (maybe specific for investigator)\n if extraToken and extraToken ~= \"None\" then\n -- local positions\n local tokenSpawnPos = {\n action = {\n Vector(-0.86, 0, -0.28), -- left of the regular three actions\n Vector(-1.54, 0, -0.28), -- right of the regular three actions\n },\n ability = {\n Vector(-1, 0, 0.118), -- bottom left corner of the investigator card\n Vector(-1, 0, -0.118), -- top left corner of the investigator card\n }\n }\n\n -- spawn tokens (split string by \"|\")\n local count = { action = 0, ability = 0 }\n for str in string.gmatch(extraToken, \"([^|]+)\") do\n local type = \"action\"\n if str == \"FreeTrigger\" or str == \"Reaction\" then\n type = \"ability\"\n end\n\n count[type] = count[type] + 1\n if count[type] \u003e 2 then\n printToColor(\"More than two extra tokens of the same type are not supported.\", playerColor)\n else\n local localSpawnPos = tokenSpawnPos[type][count[type]]\n local globalSpawnPos = self.positionToWorld(localSpawnPos):add(Vector(0, 0.2, 0))\n\n tokenManager.spawnToken(globalSpawnPos, \"universalActionAbility\", self.getRotation(),\n function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = activeInvestigatorClass, symbol = str })\n end)\n end\n end\n end\nend\n\n-- updates the texture of the playermat\n---@param overrideName? string Force a specific texture\nfunction updateTexture(overrideName)\n local name = \"Neutral\"\n\n -- use class specific texture if enabled\n if isClassTextureEnabled then\n name = activeInvestigatorClass\n end\n\n -- get new texture URL\n local newUrl = nameToTexture[name]\n\n -- override name if valid\n if nameToTexture[overrideName] then\n newUrl = nameToTexture[overrideName]\n end\n\n -- apply texture\n local customInfo = self.getCustomObject()\n if customInfo.image ~= newUrl then\n -- temporarily lock objects so they don't fall through the mat\n local objectsToUnlock = {}\n for _, obj in ipairs(searchAroundSelf()) do\n if not obj.getLock() then\n obj.setLock(true)\n table.insert(objectsToUnlock, obj)\n end\n end\n\n self.script_state = onSave()\n customInfo.image = newUrl\n self.setCustomObject(customInfo)\n local reloadedMat = self.reload()\n\n -- unlock objects when mat is reloaded\n Wait.condition(function()\n for _, obj in ipairs(objectsToUnlock) do\n obj.setLock(false)\n end\n end, function() return reloadedMat.loading_custom == false end)\n end\nend\n\n---------------------------------------------------------\n-- manipulation of owned objects\n---------------------------------------------------------\n\n-- updates the specified owned counter\n---@param param table Contains the information to update:\n--- type: String Counter to target\n--- newValue: Number Value to set the counter to\n--- modifier: Number If newValue is not provided, the existing value will be adjusted by this modifier\nfunction updateCounter(param)\n local counter = ownedObjects[param.type]\n if counter ~= nil then\n counter.call(\"updateVal\", param.newValue or (counter.getVar(\"val\") + param.modifier))\n else\n printToAll(param.type .. \" for \" .. matColor .. \" could not be found.\", \"Yellow\")\n end\nend\n\n-- get the value the specified owned counter\n---@param type string Counter to target\n---@return number: Counter value\nfunction getCounterValue(type)\n return ownedObjects[type].getVar(\"val\")\nend\n\n-- set investigator skill tracker to \"1, 1, 1, 1\"\nfunction resetSkillTracker()\n local obj = ownedObjects.InvestigatorSkillTracker\n if obj ~= nil then\n obj.call(\"updateStats\", { 1, 1, 1, 1 })\n else\n printToAll(\"Skill tracker for \" .. matColor .. \" playermat could not be found.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- calls to 'Global' / functions for calls from outside\n---------------------------------------------------------\n\nfunction drawChaosTokenButton(_, _, isRightClick)\n chaosBagApi.drawChaosToken(self, isRightClick)\nend\n\nfunction drawEncounterCard(_, _, isRightClick)\n local drawPos = getEncounterCardDrawPosition(not isRightClick)\n mythosAreaApi.drawEncounterCard(matColor, drawPos)\nend\n\nfunction returnGlobalDiscardPosition()\n return self.positionToWorld(DISCARD_PILE_POSITION)\nend\n\nfunction returnGlobalDrawPosition()\n return self.positionToWorld(DRAW_DECK_POSITION)\nend\n\n-- returns the position for encounter card drawing\n---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\nfunction getEncounterCardDrawPosition(stack)\n local drawPos = self.positionToWorld(DRAWN_ENCOUNTER_POSITION)\n\n -- maybe override position with first empty slot in threat area (right to left)\n if not stack then\n local searchPos = Vector(-0.91, 0.5, -0.625)\n for i = 1, 5 do\n local globalSearchPos = self.positionToWorld(searchPos)\n local searchResult = searchLib.atPosition(globalSearchPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n drawPos = globalSearchPos\n break\n else\n searchPos.x = searchPos.x + 0.455\n end\n end\n end\n\n return drawPos\nend\n\n-- creates / removes the draw 1 button\n---@param visible boolean Whether the draw 1 button should be visible\nfunction showDrawButton(visible)\n isDrawButtonVisible = visible\n\n if isDrawButtonVisible then\n -- Draw 1 button: modified default data\n buttonParameters.label = \"Draw 1\"\n buttonParameters.click_function = \"doDrawOne\"\n buttonParameters.tooltip = \"\"\n buttonParameters.position.z = -0.35\n self.createButton(buttonParameters)\n else\n local buttons = self.getButtons()\n for i = 1, #buttons do\n if buttons[i].label == \"Draw 1\" then\n self.removeButton(buttons[i].index)\n end\n end\n end\nend\n\n-- shows / hides a clickable clue counter for this playermat and sets the correct amount of clues\n---@param showCounter boolean Whether the clickable clue counter should be visible\nfunction clickableClues(showCounter)\n local clickerPos = ownedObjects.ClickableClueCounter.getPosition()\n local clueCount = 0\n\n -- move clue counters\n local modY = showCounter and 0.525 or -0.525\n ownedObjects.ClickableClueCounter.setPosition(clickerPos + Vector(0, modY, 0))\n\n if showCounter then\n -- get current clue count\n clueCount = ownedObjects.ClueCounter.getVar(\"exposedValue\")\n\n -- remove clues\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n\n -- set value for clue clickers\n ownedObjects.ClickableClueCounter.call(\"updateVal\", clueCount)\n else\n -- get current clue count\n clueCount = ownedObjects.ClickableClueCounter.getVar(\"val\")\n\n -- spawn clues\n local pos = self.positionToWorld({ x = -1.12, y = 0.05, z = 0.7 })\n for i = 1, clueCount do\n pos.y = pos.y + 0.045 * i\n tokenManager.spawnToken(pos, \"clue\", self.getRotation())\n end\n end\nend\n\n-- Toggles the use of class textures\n---@param state boolean Whether the class texture should be used or not\nfunction useClassTexture(state)\n if state == isClassTextureEnabled then return end\n isClassTextureEnabled = state\n updateTexture()\nend\n\n-- removes all clues (moving tokens to the trash and setting counters to 0)\nfunction removeClues()\n ownedObjects.ClueCounter.call(\"removeAllClues\", ownedObjects.Trash)\n ownedObjects.ClickableClueCounter.call(\"updateVal\", 0)\nend\n\n-- reports the clue count\n---@param useClickableCounters boolean Controls which type of counter is getting checked\nfunction getClueCount(useClickableCounters)\n if useClickableCounters then\n return ownedObjects.ClickableClueCounter.getVar(\"val\")\n else\n return ownedObjects.ClueCounter.getVar(\"exposedValue\")\n end\nend\n\n-- Sets this playermat's snap points to limit snapping to matching card types or not. If matchTypes\n-- is true, the main card slot snap points will only snap assets, while the investigator area point\n-- will only snap Investigators. If matchTypes is false, snap points will be reset to snap all cards.\n---@param matchTypes boolean Whether snap points should only snap for the matching card types.\nfunction setLimitSnapsByType(matchTypes)\n local snaps = self.getSnapPoints()\n for i, snap in ipairs(snaps) do\n if inArea(snap.position, MAIN_PLAY_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Asset\" }\n else\n table.insert(snaps[i].tags, \"Asset\")\n end\n else\n snaps[i].tags = nil\n end\n end\n if inArea(snap.position, INVESTIGATOR_AREA) then\n local snapTags = snaps[i].tags\n if matchTypes then\n if snapTags == nil then\n snaps[i].tags = { \"Investigator\" }\n else\n table.insert(snaps[i].tags, \"Investigator\")\n end\n else\n snaps[i].tags = nil\n end\n end\n end\n self.setSnapPoints(snaps)\nend\n\n-- Simple method to check if the given point is in a specified area. Local use only\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within. See MAIN_PLAY_AREA for sample bounds definition.\n---@return boolean: True if the point is in the area defined by bounds\nfunction inArea(point, bounds)\n return (point.x \u003c bounds.upperLeft.x\n and point.x \u003e bounds.lowerRight.x\n and point.z \u003c bounds.upperLeft.z\n and point.z \u003e bounds.lowerRight.z)\nend\n\n-- called by custom data helpers to add player card data\n---@param args table Contains only one entry, the GUID of the custom data helper\nfunction updatePlayerCards(args)\n local customDataHelper = getObjectFromGUID(args[1])\n local playerCardData = customDataHelper.getTable(\"PLAYER_CARD_DATA\")\n tokenManager.addPlayerCardData(playerCardData)\nend\n\n-- returns the colored steam name or color\nfunction getColoredName(playerColor)\n local displayName = playerColor\n if Player[playerColor].steam_name then\n displayName = Player[playerColor].steam_name\n end\n\n -- add bb-code\n return \"[\" .. Color.fromString(playerColor):toHex() .. \"]\" .. displayName .. \"[-]\"\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"activeInvestigatorClass\":\"Neutral\",\"activeInvestigatorId\":\"00000\",\"isClassTextureEnabled\":true,\"isDrawButtonVisible\":false,\"playerColor\":\"Red\",\"slotData\":[\"any\",\"any\",\"any\",\"Tarot\",\"Hand (left)\",\"Hand (right)\",\"Ally\",\"any\",\"any\",\"any\",\"Accessory\",\"Arcane\",\"Arcane\",\"Body\"]}", "MeasureMovement": false, "Memo": "Red", "Name": "Custom_Tile", @@ -51792,36550 +32054,6 @@ "Value": 0, "XmlUI": "" }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -24.842802, - "rotX": 1.85032036e-7, - "rotY": 269.987671, - "rotZ": 1.15851122e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.199, - "posY": 1.55, - "posZ": -24.843, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "748245", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4e9da", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -23.6854019, - "rotX": 1.70445119e-7, - "rotY": 269.987671, - "rotZ": -1.60111384e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.199, - "posY": 1.55, - "posZ": -23.685, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "271b17", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0bcce1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1985855, - "posY": 1.55004013, - "posZ": -22.5279942, - "rotX": 359.983826, - "rotY": 269.989624, - "rotZ": 359.9841, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.199, - "posY": 1.55, - "posZ": -22.528, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5bafdf", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2375d6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1995964, - "posY": 1.60133862, - "posZ": -21.6608963, - "rotX": 359.911682, - "rotY": 269.835083, - "rotZ": 353.387177, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.2, - "posY": 1.55, - "posZ": -21.637, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "012577", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2375d6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1995964, - "posY": 1.60133862, - "posZ": -21.6608963, - "rotX": 359.911682, - "rotY": 269.835083, - "rotZ": 353.387177, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.2, - "posY": 1.55, - "posZ": 10.538, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "04765b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0bcce1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1985855, - "posY": 1.55004013, - "posZ": -22.5279942, - "rotX": 359.983826, - "rotY": 269.989624, - "rotZ": 359.9841, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.2, - "posY": 1.55, - "posZ": 9.67, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b71036", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4e9da", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -23.6854019, - "rotX": 1.70445119e-7, - "rotY": 269.987671, - "rotZ": -1.60111384e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.2, - "posY": 1.55, - "posZ": 8.513, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "1cb302", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -24.842802, - "rotX": 1.85032036e-7, - "rotY": 269.987671, - "rotZ": 1.15851122e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.2, - "posY": 1.55, - "posZ": 7.355, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbc5d4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2375d6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1995964, - "posY": 1.60133862, - "posZ": -21.6608963, - "rotX": 359.911682, - "rotY": 269.835083, - "rotZ": 353.387177, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -35.912, - "posY": 1.55, - "posZ": 24.8, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "429bb3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0bcce1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1985855, - "posY": 1.55004013, - "posZ": -22.5279942, - "rotX": 359.983826, - "rotY": 269.989624, - "rotZ": 359.9841, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -36.78, - "posY": 1.55, - "posZ": 24.8, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "183dbe", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4e9da", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -23.6854019, - "rotX": 1.70445119e-7, - "rotY": 269.987671, - "rotZ": -1.60111384e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -37.937, - "posY": 1.55, - "posZ": 24.8, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b80db6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -24.842802, - "rotX": 1.85032036e-7, - "rotY": 269.987671, - "rotZ": 1.15851122e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -39.095, - "posY": 1.55, - "posZ": 24.8, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "af1927", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2375d6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1995964, - "posY": 1.60133862, - "posZ": -21.6608963, - "rotX": 359.911682, - "rotY": 269.835083, - "rotZ": 353.387177, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.35, - "scaleY": 0.6, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -24.788, - "posY": 1.55, - "posZ": -24.8, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.35, - "scaleY": 1, - "scaleZ": 0.35 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0329cc", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0bcce1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1985855, - "posY": 1.55004013, - "posZ": -22.5279942, - "rotX": 359.983826, - "rotY": 269.989624, - "rotZ": 359.9841, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -23.92, - "posY": 1.55, - "posZ": -24.8, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5bec40", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4e9da", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -23.6854019, - "rotX": 1.70445119e-7, - "rotY": 269.987671, - "rotZ": -1.60111384e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -22.763, - "posY": 1.55, - "posZ": -24.8, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5825ca", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1986, - "posY": 1.55000019, - "posZ": -24.842802, - "rotX": 1.85032036e-7, - "rotY": 269.987671, - "rotZ": 1.15851122e-7, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -21.605, - "posY": 1.55, - "posZ": -24.8, - "rotX": 0, - "rotY": 180, - "rotZ": 0, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, { "AltLookAngle": { "x": 0, @@ -88406,9 +32124,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -88433,7 +32151,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"arkhamdb/DeckImporterMain\")\nend)\n__bundle_register(\"arkhamdb/ArkhamDb\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n\n local ArkhamDb = {}\n local internal = {}\n \n local tabooList = {}\n local configuration\n\n local RANDOM_WEAKNESS_ID = \"01000\"\n\n ---@class Request\n local Request = {}\n\n -- Sets up the ArkhamDb interface. Should be called from the parent object on load.\n ArkhamDb.initialize = function()\n configuration = internal.getConfiguration()\n Request.start({ configuration.api_uri, configuration.taboo }, function(status)\n local json = JSON.decode(internal.fixUtf16String(status.text))\n for _, taboo in pairs(json) do\n local cards = {}\n\n for _, card in pairs(JSON.decode(taboo.cards)) do\n cards[card.code] = true\n end\n\n tabooList[taboo.id] = {\n date = taboo.date_start,\n cards = cards\n }\n end\n return true, nil\n end)\n end\n\n -- Start the deck build process for the given player color and deck ID. This\n -- will retrieve the deck from ArkhamDB, and pass to a callback for processing.\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\").\n ---@param deckId string ArkhamDB deck id to be loaded\n ---@param isPrivate boolean Whether this deck is published or private on ArkhamDB\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n ---@return boolean\n ---@return string\n ArkhamDb.getDecklist = function(\n playerColor,\n deckId,\n isPrivate,\n loadNewest,\n loadInvestigators,\n callback)\n -- Get a simple card to see if the bag indexes are complete. If not, abort\n -- the deck load. The called method will handle player notification.\n local checkCard = allCardsBagApi.getCardById(\"01001\")\n if (checkCard ~= nil and checkCard.data == nil) then\n return false, \"Indexing not complete\"\n end\n\n local deckUri = {\n configuration.api_uri,\n isPrivate and configuration.private_deck or configuration.public_deck,\n deckId\n }\n\n local deck = Request.start(deckUri, function(status)\n if string.find(status.text, \"\u003c!DOCTYPE html\u003e\") then\n internal.maybePrint(\"Private deck ID \" .. deckId .. \" is not shared\", playerColor)\n return false, \"Private deck \" .. deckId .. \" is not shared\"\n end\n local json = JSON.decode(status.text)\n\n if not json then\n internal.maybePrint(\"Deck ID \" .. deckId .. \" not found\", playerColor)\n return false, \"Deck not found!\"\n end\n\n return true, json\n end)\n\n deck:with(internal.onDeckResult, playerColor, loadNewest, loadInvestigators, callback)\n end\n\n -- Logs that a card could not be loaded in the mod by printing it to the console in the given\n -- color of the player owning the deck. Attempts to look up the name on ArkhamDB for clarity,\n -- but prints the card ID if the name cannot be retrieved.\n ---@param cardId string ArkhamDB ID of the card that could not be found\n ---@param playerColor string Color of the player's deck that had the problem\n ArkhamDb.logCardNotFound = function(cardId, playerColor)\n local request = Request.start({\n configuration.api_uri,\n configuration.cards,\n cardId\n },\n function(result)\n local adbCardInfo = JSON.decode(internal.fixUtf16String(result.text))\n local cardName = adbCardInfo.real_name\n if (cardName ~= nil) then\n if (adbCardInfo.xp ~= nil and adbCardInfo.xp \u003e 0) then\n cardName = cardName .. \" (\" .. adbCardInfo.xp .. \")\"\n end\n internal.maybePrint(\"Card not found: \" .. cardName .. \", card ID \" .. cardId, playerColor)\n else\n internal.maybePrint(\"Card not found in ArkhamDB/Index, ID \" .. cardId, playerColor)\n end\n end)\n end\n\n -- Callback when the deck information is received from ArkhamDB. Parses the\n -- response then applies standard transformations to the deck such as adding\n -- random weaknesses and checking for taboos. Once the deck is processed,\n -- passes to loadCards to actually spawn the defined deck.\n ---@param deck table ArkhamImportDeck\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\")\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load.\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- bondedList A table of cardID keys to meaningless values. Card IDs in this list were\n --- added from a parent bonded card.\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n internal.onDeckResult = function(deck, playerColor, loadNewest, loadInvestigators, callback)\n -- Load the next deck in the upgrade path if the option is enabled\n if (loadNewest and deck.next_deck ~= nil and deck.next_deck ~= \"\") then\n buildDeck(playerColor, deck.next_deck)\n return\n end\n\n internal.maybePrint(table.concat({ \"Found decklist: \", deck.name }), playerColor)\n\n -- Initialize deck slot table and perform common transformations. The order of these should not\n -- be changed, as later steps may act on cards added in each. For example, a random weakness or\n -- investigator may have bonded cards or taboo entries, and should be present\n local slots = deck.slots\n internal.maybeDrawRandomWeakness(slots, playerColor)\n\n -- handles alternative investigators (parallel, promo or revised art)\n local loadAltInvestigator = \"normal\"\n if loadInvestigators then\n loadAltInvestigator = internal.addInvestigatorCards(deck, slots)\n end\n\n internal.maybeModifyDeckFromDescription(slots, deck.description_md, playerColor)\n internal.maybeAddSummonedServitor(slots)\n internal.maybeAddOnTheMend(slots, playerColor)\n internal.maybeAddRealityAcidReference(slots)\n local bondList = internal.extractBondedCards(slots)\n internal.checkTaboos(deck.taboo_id, slots, playerColor)\n internal.maybeAddUpgradeSheets(slots)\n\n -- get upgrades for customizable cards\n local customizations = {}\n if deck.meta then\n customizations = JSON.decode(deck.meta)\n end\n\n callback(slots, deck.investigator_code, bondList, customizations, playerColor, loadAltInvestigator)\n end\n\n -- Checks to see if the slot list includes the random weakness ID. If it does,\n -- removes it from the deck and replaces it with the ID of a random basic weakness provided by the\n -- all cards bag\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n --- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast\n --- if a weakness is added.\n internal.maybeDrawRandomWeakness = function(slots, playerColor)\n local randomWeaknessAmount = slots[RANDOM_WEAKNESS_ID] or 0\n slots[RANDOM_WEAKNESS_ID] = nil\n\n if randomWeaknessAmount ~= 0 then\n for i=1, randomWeaknessAmount do\n local weaknessId = allCardsBagApi.getRandomWeaknessId()\n slots[weaknessId] = (slots[weaknessId] or 0) + 1\n end\n internal.maybePrint(\"Added \" .. randomWeaknessAmount .. \" random basic weakness(es) to deck\", playerColor)\n end\n end\n\n -- Adds both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each\n ---@param deck table The processed ArkhamDB deck response\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the\n --- number of those cards which will be spawned\n ---@return string: Contains the name of the art that should be loaded (\"normal\", \"promo\" or \"revised\")\n internal.addInvestigatorCards = function(deck, slots)\n local investigatorId = deck.investigator_code\n slots[investigatorId .. \"-m\"] = 1\n local deckMeta = JSON.decode(deck.meta)\n\n -- handling alternative investigator art and parallel investigators\n local loadAltInvestigator = \"normal\"\n if deckMeta ~= nil then\n local altFrontId = tonumber(deckMeta.alternate_front) or 0\n local altBackId = tonumber(deckMeta.alternate_back) or 0\n local altArt = { front = \"normal\", back = \"normal\" }\n\n -- translating front ID\n if altFrontId \u003e 90000 and altFrontId \u003c 90100 then\n altArt.front = \"parallel\"\n elseif altFrontId \u003e 01500 and altFrontId \u003c 01506 then\n altArt.front = \"revised\"\n elseif altFrontId \u003e 98000 then\n altArt.front = \"promo\"\n end\n\n -- translating back ID\n if altBackId \u003e 90000 and altBackId \u003c 90100 then\n altArt.back = \"parallel\"\n elseif altBackId \u003e 01500 and altBackId \u003c 01506 then\n altArt.back = \"revised\"\n elseif altBackId \u003e 98000 then\n altArt.back = \"promo\"\n end\n\n -- updating investigatorID based on alt investigator selection\n -- precedence: parallel \u003e promo \u003e revised\n if altArt.front == \"parallel\" then\n if altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-p\"\n else\n investigatorId = investigatorId .. \"-pf\"\n end\n elseif altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-pb\"\n elseif altArt.front == \"promo\" or altArt.back == \"promo\" then\n loadAltInvestigator = \"promo\"\n elseif altArt.front == \"revised\" or altArt.back == \"revised\" then\n loadAltInvestigator = \"revised\"\n end\n end\n slots[investigatorId] = 1\n deck.investigator_code = investigatorId\n return loadAltInvestigator\n end\n\n -- Process the card list looking for the customizable cards, and add their upgrade sheets if needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddUpgradeSheets = function(slots)\n for cardId, _ in pairs(slots) do\n -- upgrade sheets for customizable cards\n local upgradesheet = allCardsBagApi.getCardById(cardId .. \"-c\")\n if upgradesheet ~= nil then\n slots[cardId .. \"-c\"] = 1\n end\n end\n end\n\n -- Process the card list looking for the Summoned Servitor, and add its minicard to the list if\n -- needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddSummonedServitor = function(slots)\n if slots[\"09080\"] ~= nil then\n slots[\"09080-m\"] = 1\n end\n end\n\n -- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1. Update\n -- the count based on the investigator count\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast if an error occurs\n internal.maybeAddOnTheMend = function(slots, playerColor)\n if slots[\"09006\"] ~= nil then\n local investigatorCount = playAreaApi.getInvestigatorCount()\n if investigatorCount ~= nil then\n slots[\"09006\"] = investigatorCount\n else\n internal.maybePrint(\"Something went wrong with the load, adding 4 copies of On the Mend\", playerColor)\n slots[\"09006\"] = 4\n end\n end\n end\n\n -- Process the card list looking for Reality Acid and adds the reference sheet when needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddRealityAcidReference = function(slots)\n if slots[\"89004\"] ~= nil then\n slots[\"89005\"] = 1\n end\n end\n\n -- Processes the deck description from ArkhamDB and modifies the slot list accordingly\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n ---@param description string The deck desription from ArkhamDB\n internal.maybeModifyDeckFromDescription = function(slots, description, playerColor)\n -- check for import instructions\n local pos = string.find(description, \"++SCED import instructions++\")\n if not pos then return end\n\n -- remove everything before instructions\n local tempStr = string.sub(description, pos)\n\n -- parse each line in instructions\n for line in tempStr:gmatch(\"([^\\n]+)\") do\n -- remove dashes at the start\n line = line:gsub(\"%- \", \"\")\n\n -- remove spaces\n line = line:gsub(\"%s\", \"\")\n\n -- remove balanced brackets\n line = line:gsub(\"%b()\", \"\")\n line = line:gsub(\"%b[]\", \"\")\n\n -- get instructor\n local instructor = \"\"\n for word in line:gmatch(\"%a+:\") do\n instructor = word\n break\n end\n\n -- go to the next line if no valid instructor found\n if instructor ~= \"add:\" and instructor ~= \"remove:\" then\n goto nextLine\n end\n\n -- remove instructor from line\n line = line:gsub(instructor, \"\")\n\n -- evaluate instructions\n for str in line:gmatch(\"([^,]+)\") do\n if instructor == \"add:\" then\n slots[str] = (slots[str] or 0) + 1\n elseif instructor == \"remove:\" then\n if slots[str] == nil then\n internal.maybePrint(\"Tried to remove card ID \" .. str .. \", but didn't find card in deck.\", playerColor)\n else\n slots[str] = math.max(slots[str] - 1, 0)\n\n -- fully remove cards that have a quantity of 0\n if slots[str] == 0 then\n slots[str] = nil\n\n -- also remove related minicard\n slots[str .. \"-m\"] = nil\n end\n end\n end\n end\n\n -- jump mark at the end of the loop\n ::nextLine::\n end\n end\n\n -- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.extractBondedCards = function(slots)\n -- Create a list of bonded cards first so we don't modify slots while iterating\n local bondedCards = { }\n local bondedList = { }\n for cardId, cardCount in pairs(slots) do\n local card = allCardsBagApi.getCardById(cardId)\n if card ~= nil and card.metadata.bonded ~= nil then\n for _, bond in ipairs(card.metadata.bonded) do\n -- 'unlimited' upper limit for cards without this data\n local maxCount = bond.maxCount or 99\n\n -- add a bonded card for each copy of the parent card (until the max value is reached)\n bondedCards[bond.id] = math.min(bond.count * cardCount, maxCount)\n\n -- We need to know which cards are bonded to determine their position, remember them\n bondedList[bond.id] = true\n\n -- Also adding taboo versions of bonded cards to the list\n bondedList[bond.id .. \"-t\"] = true\n end\n end\n end\n\n -- Add any bonded cards to the main slots list\n for bondedId, bondedCount in pairs(bondedCards) do\n slots[bondedId] = bondedCount\n end\n\n return bondedList\n end\n\n -- Check the deck for cards on its taboo list. If they're found, replace the entry in the slot with the Taboo id (i.e. \"XXXX\" becomes \"XXXX-t\")\n ---@param tabooId string The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.checkTaboos = function(tabooId, slots, playerColor)\n if tabooId then\n for cardId, _ in pairs(tabooList[tabooId].cards) do\n if slots[cardId] ~= nil then\n -- Make sure there's a taboo version of the card before we replace it\n -- SCED only maintains the most recent taboo cards. If a deck is using\n -- an older taboo list it's possible the card isn't a taboo any more\n local tabooCard = allCardsBagApi.getCardById(cardId .. \"-t\")\n if tabooCard == nil then\n local basicCard = allCardsBagApi.getCardById(cardId)\n internal.maybePrint(\"Taboo version for \" .. basicCard.data.Nickname .. \" is not available. Using standard version\", playerColor)\n else\n slots[cardId .. \"-t\"] = slots[cardId]\n slots[cardId] = nil\n end\n end\n end\n end\n end\n\n internal.maybePrint = function(message, playerColor)\n if playerColor ~= \"None\" then\n printToAll(message, playerColor)\n end\n end\n\n -- Gets the ArkhamDB config info from the configuration object.\n ---@return table: configuration data\n internal.getConfiguration = function()\n local configuration = getObjectsWithTag(\"import_configuration_provider\")[1].getTable(\"configuration\")\n printPriority = configuration.priority\n return configuration\n end\n\n internal.fixUtf16String = function(str)\n return str:gsub(\"\\\\u(%w%w%w%w)\", function(match)\n return string.char(tonumber(match, 16))\n end)\n end\n\n Request = {\n is_done = false,\n is_successful = false\n }\n\n -- Creates a new instance of a Request. Should not be directly called. Instead use Request.start() and Request.deferred().\n ---@param uri table\n ---@param configure fun(request, status)\n ---@return Request\n function Request:new(uri, configure)\n local this = {}\n\n setmetatable(this, self)\n self.__index = self\n\n if type(uri) == \"table\" then\n uri = table.concat(uri, \"/\")\n end\n\n this.uri = uri\n WebRequest.get(uri, function(status) configure(this, status) end)\n\n return this\n end\n\n -- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.\n -- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)\n ---@param uri table\n ---@param on_success fun(request, status, vararg)\n ---@param on_error fun(status)|nil\n ---@return Request\n function Request.deferred(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request:new(uri, function(request, status)\n if (status.is_done) then\n if (status.is_error) then\n request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error\n request.is_successful = false\n request.is_done = true\n else\n on_success(request, status)\n end\n end\n end)\n end\n\n -- Creates a new request. on_success should return whether the resultant data is as expected, and the processed content of the request.\n ---@param uri table\n ---@param on_success fun(status, vararg): boolean, any\n ---@param on_error nil|fun(status, vararg): string\n ---@vararg any\n ---@return Request\n function Request.start(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request.deferred(uri, function(request, status)\n local result, message = on_success(status, table.unpack(parameters))\n if not result then request.error_message = message else request.content = message end\n request.is_successful = result\n request.is_done = true\n end, on_error, table.unpack(parameters))\n end\n\n ---@param requests Request[]\n ---@param on_success fun(content: any, vararg: any)\n ---@param on_error fun(requests: Request, vararg: any)|nil\n ---@vararg any\n function Request.with_all(requests, on_success, on_error, ...)\n local parameters = table.pack(...)\n\n Wait.condition(function()\n local results = {}\n local errors = {}\n\n for _, request in ipairs(requests) do\n if request.is_successful then\n table.insert(results, request.content)\n else\n table.insert(errors, request)\n end\n end\n\n if (#errors \u003c= 0) then\n on_success(results, table.unpack(parameters))\n elseif on_error == nil then\n for _, request in ipairs(errors) do\n internal.maybePrint(table.concat({ \"[ERROR]\", request.uri, \":\", request.error_message }))\n end\n else\n on_error(requests, table.unpack(parameters))\n end\n end, function()\n for _, request in ipairs(requests) do\n if not request.is_done then return false end\n end\n return true\n end)\n end\n\n function Request:with(callback, ...)\n local arguments = table.pack(...)\n Wait.condition(function()\n if self.is_successful then\n callback(self.content, table.unpack(arguments))\n end\n end, function() return self.is_done\n end)\n end\n\n return ArkhamDb\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"playermat/Zones\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Sets up and returns coordinates for all possible spawn zones. Because Lua assigns tables by reference\n-- and there is no built-in function to copy a table this is relatively brute force.\n--\n-- Positions are all relative to the player mat, and most are consistent. The\n-- exception are the SetAside# zones, which are placed to the left of the mat\n-- for White/Green, and the right of the mat for Orange/Red.\n--\n-- Investigator: Investigator card area.\n-- Minicard: Placement for the investigator's minicard, just above the player mat\n-- Deck, Discard: Standard locations for the deck and discard piles.\n-- Blank1: used for assets that start in play (e.g. Duke)\n-- Tarot, Hand1, Hand2, Ally, Blank4, Accessory, Arcane1, Arcane2, Body: Asset slot positions\n-- Threat[1-4]: Threat area slots. Threat[1-3] correspond to the named threat area slots, and Threat4 is the blank threat area slot.\n-- SetAside[1-3]: Column closest to the player mat, with 1 at the top and 3 at the bottom.\n-- SetAside[4-6]: Column farther away from the mat, with 4 at the top and 6 at the bottom.\n-- SetAside1: Permanent cards\n-- SetAside2: Bonded cards\n-- SetAside3: Ancestral Knowledge / Underworld Market\n-- SetAside4: Upgrade sheets for customizable cards\n-- SetAside5: Hunch Deck for Joe Diamond\n-- SetAside6: currently unused\ndo\n local playmatApi = require(\"playermat/PlaymatApi\")\n local Zones = { }\n\n local commonZones = {}\n commonZones[\"Investigator\"] = { -1.177, 0, 0.002 }\n commonZones[\"Deck\"] = { -1.82, 0, 0 }\n commonZones[\"Discard\"] = { -1.82, 0, 0.61 }\n commonZones[\"Ally\"] = { -0.615, 0, 0.024 }\n commonZones[\"Body\"] = { -0.630, 0, 0.553 }\n commonZones[\"Hand1\"] = { 0.215, 0, 0.042 }\n commonZones[\"Hand2\"] = { -0.180, 0, 0.037 }\n commonZones[\"Arcane1\"] = { 0.212, 0, 0.559 }\n commonZones[\"Arcane2\"] = { -0.171, 0, 0.557 }\n commonZones[\"Tarot\"] = { 0.602, 0, 0.033 }\n commonZones[\"Accessory\"] = { 0.602, 0, 0.555 }\n commonZones[\"Blank1\"] = { 1.758, 0, 0.040 }\n commonZones[\"Blank2\"] = { 1.754, 0, 0.563 }\n commonZones[\"Blank3\"] = { 1.371, 0, 0.038 }\n commonZones[\"Blank4\"] = { 1.371, 0, 0.558 }\n commonZones[\"Blank5\"] = { 0.98, 0, 0.035 }\n commonZones[\"Blank6\"] = { 0.977, 0, 0.556 }\n commonZones[\"Threat1\"] = { -0.911, 0, -0.625 }\n commonZones[\"Threat2\"] = { -0.454, 0, -0.625 }\n commonZones[\"Threat3\"] = { 0.002, 0, -0.625 }\n commonZones[\"Threat4\"] = { 0.459, 0, -0.625 }\n\n local zoneData = {}\n zoneData[\"White\"] = {}\n zoneData[\"White\"][\"Investigator\"] = commonZones[\"Investigator\"]\n zoneData[\"White\"][\"Deck\"] = commonZones[\"Deck\"]\n zoneData[\"White\"][\"Discard\"] = commonZones[\"Discard\"]\n zoneData[\"White\"][\"Ally\"] = commonZones[\"Ally\"]\n zoneData[\"White\"][\"Body\"] = commonZones[\"Body\"]\n zoneData[\"White\"][\"Hand1\"] = commonZones[\"Hand1\"]\n zoneData[\"White\"][\"Hand2\"] = commonZones[\"Hand2\"]\n zoneData[\"White\"][\"Arcane1\"] = commonZones[\"Arcane1\"]\n zoneData[\"White\"][\"Arcane2\"] = commonZones[\"Arcane2\"]\n zoneData[\"White\"][\"Tarot\"] = commonZones[\"Tarot\"]\n zoneData[\"White\"][\"Accessory\"] = commonZones[\"Accessory\"]\n zoneData[\"White\"][\"Blank1\"] = commonZones[\"Blank1\"]\n zoneData[\"White\"][\"Blank2\"] = commonZones[\"Blank2\"]\n zoneData[\"White\"][\"Blank3\"] = commonZones[\"Blank3\"]\n zoneData[\"White\"][\"Blank4\"] = commonZones[\"Blank4\"]\n zoneData[\"White\"][\"Blank5\"] = commonZones[\"Blank5\"]\n zoneData[\"White\"][\"Blank6\"] = commonZones[\"Blank6\"]\n zoneData[\"White\"][\"Threat1\"] = commonZones[\"Threat1\"]\n zoneData[\"White\"][\"Threat2\"] = commonZones[\"Threat2\"]\n zoneData[\"White\"][\"Threat3\"] = commonZones[\"Threat3\"]\n zoneData[\"White\"][\"Threat4\"] = commonZones[\"Threat4\"]\n zoneData[\"White\"][\"Minicard\"] = { -1, 0, -1.45 }\n zoneData[\"White\"][\"SetAside1\"] = { 2.35, 0, -0.520 }\n zoneData[\"White\"][\"SetAside2\"] = { 2.35, 0, 0.042 }\n zoneData[\"White\"][\"SetAside3\"] = { 2.35, 0, 0.605 }\n zoneData[\"White\"][\"UnderSetAside3\"] = { 2.50, 0, 0.805 }\n zoneData[\"White\"][\"SetAside4\"] = { 2.78, 0, -0.520 }\n zoneData[\"White\"][\"SetAside5\"] = { 2.78, 0, 0.042 }\n zoneData[\"White\"][\"SetAside6\"] = { 2.78, 0, 0.605 }\n zoneData[\"White\"][\"UnderSetAside6\"] = { 2.93, 0, 0.805 }\n\n zoneData[\"Orange\"] = {}\n zoneData[\"Orange\"][\"Investigator\"] = commonZones[\"Investigator\"]\n zoneData[\"Orange\"][\"Deck\"] = commonZones[\"Deck\"]\n zoneData[\"Orange\"][\"Discard\"] = commonZones[\"Discard\"]\n zoneData[\"Orange\"][\"Ally\"] = commonZones[\"Ally\"]\n zoneData[\"Orange\"][\"Body\"] = commonZones[\"Body\"]\n zoneData[\"Orange\"][\"Hand1\"] = commonZones[\"Hand1\"]\n zoneData[\"Orange\"][\"Hand2\"] = commonZones[\"Hand2\"]\n zoneData[\"Orange\"][\"Arcane1\"] = commonZones[\"Arcane1\"]\n zoneData[\"Orange\"][\"Arcane2\"] = commonZones[\"Arcane2\"]\n zoneData[\"Orange\"][\"Tarot\"] = commonZones[\"Tarot\"]\n zoneData[\"Orange\"][\"Accessory\"] = commonZones[\"Accessory\"]\n zoneData[\"Orange\"][\"Blank1\"] = commonZones[\"Blank1\"]\n zoneData[\"Orange\"][\"Blank2\"] = commonZones[\"Blank2\"]\n zoneData[\"Orange\"][\"Blank3\"] = commonZones[\"Blank3\"]\n zoneData[\"Orange\"][\"Blank4\"] = commonZones[\"Blank4\"]\n zoneData[\"Orange\"][\"Blank5\"] = commonZones[\"Blank5\"]\n zoneData[\"Orange\"][\"Blank6\"] = commonZones[\"Blank6\"]\n zoneData[\"Orange\"][\"Threat1\"] = commonZones[\"Threat1\"]\n zoneData[\"Orange\"][\"Threat2\"] = commonZones[\"Threat2\"]\n zoneData[\"Orange\"][\"Threat3\"] = commonZones[\"Threat3\"]\n zoneData[\"Orange\"][\"Threat4\"] = commonZones[\"Threat4\"]\n zoneData[\"Orange\"][\"Minicard\"] = { 1, 0, -1.45 }\n zoneData[\"Orange\"][\"SetAside1\"] = { -2.35, 0, -0.520 }\n zoneData[\"Orange\"][\"SetAside2\"] = { -2.35, 0, 0.042}\n zoneData[\"Orange\"][\"SetAside3\"] = { -2.35, 0, 0.605 }\n zoneData[\"Orange\"][\"UnderSetAside3\"] = { -2.50, 0, 0.805 }\n zoneData[\"Orange\"][\"SetAside4\"] = { -2.78, 0, -0.520 }\n zoneData[\"Orange\"][\"SetAside5\"] = { -2.78, 0, 0.042 }\n zoneData[\"Orange\"][\"SetAside6\"] = { -2.78, 0, 0.605 }\n zoneData[\"Orange\"][\"UnderSetAside6\"] = { -2.93, 0, 0.805 }\n\n -- Green positions are the same as White and Red the same as Orange\n zoneData[\"Red\"] = zoneData[\"Orange\"]\n zoneData[\"Green\"] = zoneData[\"White\"]\n\n -- Gets the global position for the given zone on the specified player mat.\n ---@param playerColor string Color name of the player mat to get the zone position for (e.g. \"Red\")\n ---@param zoneName string Name of the zone to get the position for. See Zones object documentation for a list of valid zones.\n ---@return tts__Vector|nil: Global position table, or nil if an invalid player color or zone is specified\n Zones.getZonePosition = function(playerColor, zoneName)\n if (playerColor ~= \"Red\"\n and playerColor ~= \"Orange\"\n and playerColor ~= \"White\"\n and playerColor ~= \"Green\") then\n return nil\n end\n return playmatApi.transformLocalPosition(zoneData[playerColor][zoneName], playerColor)\n end\n\n -- Return the global rotation for a card on the given player mat, based on its zone.\n ---@param playerColor string Color name of the player mat to get the rotation for (e.g. \"Red\")\n ---@param zoneName string Name of the zone. See Zones object documentation for a list of valid zones.\n ---@return tts__Vector: Global rotation vector for the given card. This will include the\n -- Y rotation to orient the card on the given player mat as well as a\n -- Z rotation to place the card face up or face down.\n Zones.getDefaultCardRotation = function(playerColor, zoneName)\n local cardRotation = playmatApi.returnRotation(playerColor)\n if zoneName == \"Deck\" then\n cardRotation = cardRotation + Vector(0, 0, 180)\n end\n return cardRotation\n end\n\n return Zones\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"arkhamdb/DeckImporterMain\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"arkhamdb/DeckImporterUi\")\nrequire(\"playercards/PlayerCardSpawner\")\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\nlocal arkhamDb = require(\"arkhamdb/ArkhamDb\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal zones = require(\"playermat/Zones\")\n\nlocal startsInPlayCount = 0\n\nfunction onLoad(script_state)\n initializeUi(JSON.decode(script_state))\n math.randomseed(os.time())\n arkhamDb.initialize()\nend\n\nfunction onSave() return JSON.encode(getUiState()) end\n\n-- Returns the zone name where the specified card should be placed, based on its metadata.\n---@param cardMetadata table Contains card metadata\n---@return string Zone Name of the zone such as \"Deck\", \"SetAside1\", etc.\n-- See Zones object documentation for a list of valid zones.\nfunction getDefaultCardZone(cardMetadata, bondedList)\n if (cardMetadata.id == \"09080-m\") then -- Have to check the Servitor before other minicards\n return \"SetAside6\"\n elseif (cardMetadata.id == \"09006\") then -- On The Mend is set aside\n return \"SetAside2\"\n elseif bondedList[cardMetadata.id] then\n return \"SetAside2\"\n elseif cardMetadata.type == \"Investigator\" then\n return \"Investigator\"\n elseif cardMetadata.type == \"Minicard\" then\n return \"Minicard\"\n elseif cardMetadata.type == \"UpgradeSheet\" then\n return \"SetAside4\"\n elseif cardMetadata.startsInPlay then\n return startsInPlayTracker()\n elseif cardMetadata.permanent then\n return \"SetAside1\"\n -- SetAside3 is used for Ancestral Knowledge / Underworld Market\n else\n return \"Deck\"\n end\nend\n\nfunction startsInPlayTracker()\n startsInPlayCount = startsInPlayCount + 1\n if startsInPlayCount \u003e 6 then\n broadcastToAll(\"Card that should start in play was placed with permanents because no blank slots remained\")\n return \"SetAside1\"\n else\n return \"Blank\" .. startsInPlayCount\n end\nend\n\nfunction buildDeck(playerColor, deckId)\n local uiState = getUiState()\n arkhamDb.getDecklist(\n playerColor,\n deckId,\n uiState.privateDeck,\n uiState.loadNewest,\n uiState.investigators,\n loadCards)\nend\n\n-- Process the slot list, which defines the card Ids and counts of cards to load. Spawn those cards\n-- at the appropriate zones and report an error to the user if any could not be loaded.\n-- This is a callback function which handles the results of ArkhamDb.getDecklist()\n-- This method uses an encapsulated coroutine with yields to make the card spawning cleaner.\n--\n---@param slots table Key-Value table of cardId:count. cardId is the ArkhamDB ID of the card to spawn,\n-- and count is the number which should be spawned\n---@param investigatorId string ArkhamDB ID (code) for this deck's investigator.\n-- Investigator cards should already be added to the slots list if they\n-- should be spawned, but this value is separate to check for special\n-- handling for certain investigators\n---@param bondedList table A table of cardID keys to meaningless values. Card IDs in this list were added\n-- from a parent bonded card.\n---@param customizations table ArkhamDB data for customizations on customizable cards\n---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\")\n---@param loadAltInvestigator string Contains the name of alternative art for the investigator (\"normal\", \"revised\" or \"promo\")\nfunction loadCards(slots, investigatorId, bondedList, customizations, playerColor, loadAltInvestigator)\n function coinside()\n local cardsToSpawn = {}\n\n -- reset the startsInPlayCount\n startsInPlayCount = 0\n for cardId, cardCount in pairs(slots) do\n local card = allCardsBagApi.getCardById(cardId)\n if card ~= nil then\n local cardZone = getDefaultCardZone(card.metadata, bondedList)\n for i = 1, cardCount do\n table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })\n end\n\n slots[cardId] = 0\n end\n end\n\n handleAncestralKnowledge(cardsToSpawn)\n handleUnderworldMarket(cardsToSpawn, playerColor)\n handleHunchDeck(investigatorId, cardsToSpawn, bondedList, playerColor)\n handleSpiritDeck(investigatorId, cardsToSpawn, playerColor, customizations)\n handleCustomizableUpgrades(cardsToSpawn, customizations)\n handlePeteSignatureAssets(investigatorId, cardsToSpawn)\n\n -- Split the card list into separate lists for each zone\n local zoneDecks = buildZoneLists(cardsToSpawn)\n -- Spawn the list for each zone\n for zone, zoneCards in pairs(zoneDecks) do\n local deckPos = zones.getZonePosition(playerColor, zone):setAt(\"y\", 3)\n local deckRot = zones.getDefaultCardRotation(playerColor, zone)\n\n local callback = nil\n -- If cards are spread too close together TTS groups them weirdly, selecting multiples\n -- when hovering over a single card. This distance is the minimum to avoid that\n local spreadDistance = 1.15\n if (zone == \"SetAside4\") then\n -- SetAside4 is reserved for customization cards, and we want them spread on the table\n -- so their checkboxes are visible\n -- TO-DO: take into account that spreading will make multiple rows\n -- (this is affected by the user's local settings!)\n if (playerColor == \"White\") then\n deckPos.z = deckPos.z + (#zoneCards - 1) * spreadDistance\n elseif (playerColor == \"Green\") then\n deckPos.x = deckPos.x + (#zoneCards - 1) * spreadDistance\n end\n callback = function(deck) deck.spread(spreadDistance) end\n elseif zone == \"Deck\" then\n callback = function(deck) deckSpawned(deck, playerColor) end\n elseif zone == \"Investigator\" or zone == \"Minicard\" then\n callback = function(card) loadAltArt(card, loadAltInvestigator) end\n end\n Spawner.spawnCards(zoneCards, deckPos, deckRot, true, callback)\n coroutine.yield(0)\n end\n\n -- Look for any cards which haven't been loaded\n local hadError = false\n for cardId, remainingCount in pairs(slots) do\n if remainingCount \u003e 0 then\n hadError = true\n arkhamDb.logCardNotFound(cardId, playerColor)\n end\n end\n if (not hadError) then\n printToAll(\"Deck loaded successfully!\", playerColor)\n end\n return 1\n end\n\n startLuaCoroutine(self, \"coinside\")\nend\n\n-- Callback handler for the main deck spawning. Looks for cards which should start in hand, and\n-- draws them for the appropriate player.\n---@param deck tts__Object Callback-provided spawned deck object\n---@param playerColor string Color of the player to draw the cards to\nfunction deckSpawned(deck, playerColor)\n local player = Player[playmatApi.getPlayerColor(playerColor)]\n local handPos = player.getHandTransform(1).position -- Only one hand zone per player\n local deckCards = deck.getData().ContainedObjects\n\n -- Process in reverse order so taking cards out doesn't upset the indexing\n for i = #deckCards, 1, -1 do\n local cardMetadata = JSON.decode(deckCards[i].GMNotes) or { }\n if cardMetadata.startsInHand then\n deck.takeObject({ index = i - 1, position = handPos, flip = true, smooth = true})\n end\n end\n\n -- add the \"PlayerCard\" tag to the deck\n if deck and deck.type == \"Deck\" and deck.getQuantity() \u003e 1 then\n deck.addTag(\"PlayerCard\")\n end\nend\n\n-- Converts the Raven Quill's selections from card IDs to card names. This could be more elegant\n-- but the inputs are very static so we're using some brute force.\n---@param selectionString string provided by ArkhamDB, indicates the customization selections\n-- Should be either a single card ID or two separated by a ^ (e.g. XXXXX^YYYYY)\nfunction convertRavenQuillSelections(selectionString)\n if (string.len(selectionString) == 5) then\n return getCardName(selectionString)\n elseif (string.len(selectionString) == 11) then\n return getCardName(string.sub(selectionString, 1, 5)) .. \", \" .. getCardName(string.sub(selectionString, 7))\n end\nend\n\n-- Converts Grizzled's selections from a single string with \"^\".\n---@param selectionString string provided by ArkhamDB, indicates the customization selections\n-- Should be two Traits separated by a ^ (e.g. XXXXX^YYYYY)\nfunction convertGrizzledSelections(selectionString)\n return selectionString:gsub(\"%^\", \", \")\nend\n\n-- Returns the simple name of a card given its ID. This will find the card and strip any trailing\n-- SCED-specific suffixes such as (Taboo) or (Level)\nfunction getCardName(cardId)\n local card = allCardsBagApi.getCardById(cardId)\n if (card ~= nil) then\n local name = card.data.Nickname\n if (string.find(name, \" %(\")) then\n return string.sub(name, 1, string.find(name, \" %(\") - 1)\n else\n return name\n end\n end\nend\n\n-- Split a single list of cards into a separate table of lists, keyed by the zone\n---@param cards table Table of {cardData, cardMetadata, zone}\n---@return table ZoneNames Table with zoneName as index: {zoneName=card list}\nfunction buildZoneLists(cards)\n local zoneList = {}\n for _, card in ipairs(cards) do\n if zoneList[card.zone] == nil then\n zoneList[card.zone] = {}\n end\n table.insert(zoneList[card.zone], card)\n end\n\n return zoneList\nend\n\n-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3\n---@param cardList table Deck list being created\nfunction handleAncestralKnowledge(cardList)\n local hasAncestralKnowledge = false\n local skillList = {}\n -- Have to process the entire list to check for Ancestral Knowledge and get all possible skills, so do both in one pass\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"07303\" then\n hasAncestralKnowledge = true\n card.zone = \"SetAside3\"\n elseif (card.metadata.type == \"Skill\"\n and card.zone == \"Deck\"\n and not card.metadata.weakness) then\n table.insert(skillList, i)\n end\n end\n\n if not hasAncestralKnowledge then return end\n\n for i = 1, 5 do\n -- Move 5 random skills to SetAside3\n local skillListIndex = math.random(#skillList)\n cardList[skillList[skillListIndex]].zone = \"UnderSetAside3\"\n table.remove(skillList, skillListIndex)\n end\nend\n\n-- Check for and handle Underworld Market by moving all Illicit cards to UnderSetAside3\n---@param cardList table Deck list being created\n---@param playerColor string Color this deck is being loaded for\nfunction handleUnderworldMarket(cardList, playerColor)\n local hasMarket = false\n local illicitList = {}\n -- Process the entire list to check for Underworld Market and get all possible Illicit cards, doing both in one pass\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"09077\" then\n -- Underworld Market found\n hasMarket = true\n card.zone = \"SetAside3\"\n elseif card.metadata.traits ~= nil and string.find(card.metadata.traits, \"Illicit\", 1, true) and card.zone == \"Deck\" then\n table.insert(illicitList, i)\n end\n end\n\n if not hasMarket then return end\n\n if #illicitList \u003c 10 then\n printToAll(\"Only \" .. #illicitList .. \" Illicit cards in your deck, you can't trigger Underworld Market's ability.\", playerColor)\n else\n -- Process cards to move them to the market deck. This is done in reverse\n -- order because the sorting needs to be reversed (deck sorts for face down)\n -- Performance here may be an issue, as table.remove() is an O(n) operation\n -- which makes the full shift O(n^2). But keep it simple unless it becomes\n -- a problem\n for i = #illicitList, 1, -1 do\n local moving = cardList[illicitList[i]]\n moving.zone = \"UnderSetAside3\"\n table.remove(cardList, illicitList[i])\n table.insert(cardList, moving)\n end\n\n if #illicitList \u003e 10 then\n printToAll(\"Moved all \" .. #illicitList .. \" Illicit cards to the Market deck, reduce it to 10\", playerColor)\n else\n printToAll(\"Built the Market deck\", playerColor)\n end\n end\nend\n\n-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch Deck\n---@param investigatorId string ID for the deck's investigator card. Passed separately because the\n--- investigator may not be included in the cardList\n---@param cardList table Deck list being created\n---@param playerColor string Color this deck is being loaded for\nfunction handleHunchDeck(investigatorId, cardList, bondedList, playerColor)\n if investigatorId ~= \"05002\" then return end\n\n local insightList = {}\n for i, card in ipairs(cardList) do\n if (card.metadata.type == \"Event\"\n and card.metadata.traits ~= nil\n and string.match(card.metadata.traits, \"Insight\")\n and bondedList[card.metadata.id] == nil) then\n table.insert(insightList, i)\n end\n end\n -- Process insights to move them to the hunch deck. This is done in reverse\n -- order because the sorting needs to be reversed (deck sorts for face down)\n -- Performance here may be an issue, as table.remove() is an O(n) operation\n -- which makes the full shift O(n^2). But keep it simple unless it becomes\n -- a problem\n for i = #insightList, 1, -1 do\n local moving = cardList[insightList[i]]\n moving.zone = \"SetAside5\"\n table.remove(cardList, insightList[i])\n table.insert(cardList, moving)\n end\n\n if #insightList \u003c 11 then\n printToAll(\"Joe's hunch deck must have 11 cards but the deck only has \" .. #insightList .. \" Insight events.\", playerColor)\n elseif #insightList \u003e 11 then\n printToAll(\"Moved all \" .. #insightList .. \" Insight events to the hunch deck, reduce it to 11.\", playerColor)\n else\n printToAll(\"Built Joe's hunch deck\", playerColor)\n end\nend\n\n-- If the investigator is Parallel Jim Culver, extract all Ally assets to SetAside5 to build the Spirit Deck\n---@param investigatorId string ID for the deck's investigator card. Passed separately because the\n--- investigator may not be included in the cardList\n---@param cardList table Deck list being created\n---@param playerColor string Color this deck is being loaded for\n---@param customizations table Additional deck information\nfunction handleSpiritDeck(investigatorId, cardList, playerColor, customizations)\n if investigatorId ~= \"02004-p\" and investigatorId ~= \"02004-pb\" then return end\n\n local spiritList = {}\n if customizations[\"extra_deck\"] then\n -- split by \",\"\n for str in string.gmatch(customizations[\"extra_deck\"], \"([^,]+)\") do\n local card = allCardsBagApi.getCardById(str)\n if card ~= nil then\n table.insert(cardList, { data = card.data, metadata = card.metadata, zone = \"SetAside5\" })\n table.insert(spiritList, str)\n end\n end\n else\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"90053\" or (\n card.metadata.type == \"Asset\"\n and card.metadata.traits ~= nil\n and string.match(card.metadata.traits, \"Ally\")\n and card.metadata.level ~= nil\n and card.metadata.level \u003c 3) then\n table.insert(spiritList, i)\n end\n end\n\n -- Process allies to move them to the spirit deck. This is done in reverse\n -- order because the sorting needs to be reversed (deck sorts for face down)\n -- Performance here may be an issue, as table.remove() is an O(n) operation\n -- which makes the full shift O(n^2). But keep it simple unless it becomes\n -- a problem\n for i = #spiritList, 1, -1 do\n local moving = cardList[spiritList[i]]\n moving.zone = \"SetAside5\"\n table.remove(cardList, spiritList[i])\n table.insert(cardList, moving)\n end\n end\n\n if #spiritList \u003c 10 then\n printToAll(\"Jim's spirit deck must have 9 Ally assets but the deck only has \" .. (#spiritList - 1) .. \" Ally assets.\", playerColor)\n elseif #spiritList \u003e 11 then\n printToAll(\"Moved all \" .. (#spiritList - 1) .. \" Ally assets to the spirit deck, reduce it to 10 (including Vengeful Shade).\", playerColor)\n else\n printToAll(\"Built Jim's spirit deck\", playerColor)\n end\nend\n\n-- For any customization upgrade cards in the card list, process the metadata from the deck to\n-- set the save state to show the correct checkboxes/text field values\n---@param cardList table Deck list being created\n---@param customizations table ArkhamDB data for customizations on customizable cards\nfunction handleCustomizableUpgrades(cardList, customizations)\n for _, card in ipairs(cardList) do\n if card.metadata.type == \"UpgradeSheet\" then\n local baseId = string.sub(card.metadata.id, 1, 5)\n local upgrades = customizations[\"cus_\" .. baseId]\n\n if upgrades ~= nil then\n -- initialize tables\n -- markedBoxes: contains the amount of markedBoxes (left to right) per row (starting at row 1)\n -- inputValues: contains the amount of inputValues per row (starting at row 0)\n local selectedUpgrades = { }\n local index_xp = {}\n\n -- get the index and xp values (looks like this: X|X,X|X, ..)\n -- input string from ArkhamDB is split by \",\"\n for str in string.gmatch(customizations[\"cus_\" .. baseId], \"([^,]+)\") do\n table.insert(index_xp, str)\n end\n\n -- split each pair and assign it to the proper position in markedBoxes\n for _, entry in ipairs(index_xp) do\n -- counter increments from 1 to 3 and indicates the part of the string we are on\n -- usually: 1 = row, 2 = amount of check boxes, 3 = entry in inputfield\n local counter = 0\n local row = 0\n\n -- parsing the string for each row\n for str in entry:gmatch(\"([^|]+)\") do\n counter = counter + 1\n\n if counter == 1 then\n row = tonumber(str) + 1\n elseif counter == 2 then\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n end\n selectedUpgrades[row].xp = tonumber(str)\n elseif counter == 3 and str ~= \"\" then\n if baseId == \"09042\" then\n selectedUpgrades[row].text = convertRavenQuillSelections(str)\n elseif baseId == \"09101\" then\n selectedUpgrades[row].text = convertGrizzledSelections(str)\n elseif baseId == \"09079\" then -- Living Ink skill selection\n -- All skills, regardless of row, are placed in upgrade slot 1 as a comma-delimited list\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n if selectedUpgrades[1].text == nil then\n selectedUpgrades[1].text = str\n else\n selectedUpgrades[1].text = selectedUpgrades[1].text .. \",\" .. str\n end\n else\n selectedUpgrades[row].text = str\n end\n end\n end\n end\n\n -- write the loaded values to the save_data of the sheets\n card.data[\"LuaScriptState\"] = JSON.encode({ selections = selectedUpgrades })\n end\n end\n end\nend\n\n-- Handles cards that start in play under specific conditions for Ashcan Pete (Regular Pete - Duke, Parallel Pete - Guitar)\n---@param investigatorId string ID for the deck's investigator card. Passed separately because the\n--- investigator may not be included in the cardList\n---@param cardList table Deck list being created\nfunction handlePeteSignatureAssets(investigatorId, cardList)\n if investigatorId == \"02005\" or investigatorId == \"02005-pb\" then -- regular Pete's front\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"02014\" then -- Duke\n card.zone = startsInPlayTracker()\n end\n end\n elseif investigatorId == \"02005-p\" or investigatorId == \"02005-pf\" then -- parallel Pete's front\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"90047\" then -- Pete's Guitar\n card.zone = startsInPlayTracker()\n end\n end\n end\nend\n\n-- Callback function for investigator cards and minicards to set the correct state for alt art\n---@param card tts__Object Card which needs to be set the state for\n---@param loadAltInvestigator string Contains the name of alternative art for the investigator (\"normal\", \"revised\" or \"promo\")\nfunction loadAltArt(card, loadAltInvestigator)\n -- states are set up this way:\n -- 1 - normal, 2 - revised/promo, 3 - promo (if 2 is revised)\n -- This means we can always load the 2nd state for revised and just get the last state for promo\n if loadAltInvestigator == \"normal\" then\n return\n elseif loadAltInvestigator == \"revised\" then\n card.setState(2)\n elseif loadAltInvestigator == \"promo\" then\n local states = card.getStates()\n card.setState(#states)\n end\nend\nend)\n__bundle_register(\"arkhamdb/DeckImporterUi\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n\nlocal INPUT_FIELD_HEIGHT = 340\nlocal INPUT_FIELD_WIDTH = 1500\nlocal FIELD_COLOR = { 0.9, 0.7, 0.5 }\n\nlocal PRIVATE_TOGGLE_LABELS = {}\nPRIVATE_TOGGLE_LABELS[true] = \"Private\"\nPRIVATE_TOGGLE_LABELS[false] = \"Published\"\n\nlocal UPGRADED_TOGGLE_LABELS = {}\nUPGRADED_TOGGLE_LABELS[true] = \"Upgraded\"\nUPGRADED_TOGGLE_LABELS[false] = \"Specific\"\n\nlocal LOAD_INVESTIGATOR_TOGGLE_LABELS = {}\nLOAD_INVESTIGATOR_TOGGLE_LABELS[true] = \"Yes\"\nLOAD_INVESTIGATOR_TOGGLE_LABELS[false] = \"No\"\n\nlocal redDeckId = \"\"\nlocal orangeDeckId = \"\"\nlocal whiteDeckId = \"\"\nlocal greenDeckId = \"\"\n\nlocal privateDeck = true\nlocal loadNewestDeck = true\nlocal loadInvestigators = false\n\n-- Returns a table with the full state of the UI, including options and deck IDs.\n-- This can be used to persist via onSave(), or provide values for a load operation\n---@return uiStateTable uiStateTable Contains data about the current UI state\nfunction getUiState()\n return {\n redDeck = redDeckId,\n orangeDeck = orangeDeckId,\n whiteDeck = whiteDeckId,\n greenDeck = greenDeckId,\n privateDeck = privateDeck,\n loadNewest = loadNewestDeck,\n investigators = loadInvestigators\n }\nend\n\n-- Updates the state of the UI based on the provided table. Any values not provided will be left the same.\n---@param uiStateTable table Table of values to update on importer\nfunction setUiState(uiStateTable)\n self.clearButtons()\n self.clearInputs()\n initializeUi(uiStateTable)\nend\n\n-- Sets up the UI for the deck loader, populating fields from the given save state table decoded from onLoad()\nfunction initializeUi(savedUiState)\n if savedUiState ~= nil then\n redDeckId = savedUiState.redDeck\n orangeDeckId = savedUiState.orangeDeck\n whiteDeckId = savedUiState.whiteDeck\n greenDeckId = savedUiState.greenDeck\n privateDeck = savedUiState.privateDeck\n loadNewestDeck = savedUiState.loadNewest\n loadInvestigators = savedUiState.investigators\n end\n\n makeOptionToggles()\n makeDeckIdFields()\n makeBuildButton()\nend\n\nfunction makeOptionToggles()\n -- common parameters\n local checkbox_parameters = {}\n checkbox_parameters.function_owner = self\n checkbox_parameters.width = INPUT_FIELD_WIDTH\n checkbox_parameters.height = INPUT_FIELD_HEIGHT\n checkbox_parameters.scale = { 0.1, 0.1, 0.1 }\n checkbox_parameters.font_size = 240\n checkbox_parameters.hover_color = { 0.4, 0.6, 0.8 }\n checkbox_parameters.color = FIELD_COLOR\n\n -- public / private deck\n checkbox_parameters.click_function = \"publicPrivateChanged\"\n checkbox_parameters.position = { 0.25, 0.1, -0.102 }\n checkbox_parameters.tooltip = \"Published or private deck?\\n\\nPLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!\"\n checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck]\n self.createButton(checkbox_parameters)\n\n -- load upgraded?\n checkbox_parameters.click_function = \"loadUpgradedChanged\"\n checkbox_parameters.position = { 0.25, 0.1, -0.01 }\n checkbox_parameters.tooltip = \"Load newest upgrade or exact deck?\"\n checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck]\n self.createButton(checkbox_parameters)\n\n -- load investigators?\n checkbox_parameters.click_function = \"loadInvestigatorsChanged\"\n checkbox_parameters.position = { 0.25, 0.1, 0.081 }\n checkbox_parameters.tooltip = \"Spawn investigator cards?\"\n checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators]\n self.createButton(checkbox_parameters)\nend\n\n-- Create the four deck ID entry fields\nfunction makeDeckIdFields()\n local input_parameters = {}\n -- Parameters common to all entry fields\n input_parameters.function_owner = self\n input_parameters.scale = { 0.1, 0.1, 0.1 }\n input_parameters.width = INPUT_FIELD_WIDTH\n input_parameters.height = INPUT_FIELD_HEIGHT\n input_parameters.font_size = 320\n input_parameters.tooltip = \"Deck ID from ArkhamDB URL of the deck\\nPublic URL: 'https://arkhamdb.com/decklist/view/101/knowledge-overwhelming-solo-deck-1.0' = '101'\\nPrivate URL: 'https://arkhamdb.com/deck/view/102' = '102'\"\n input_parameters.alignment = 3 -- Center\n input_parameters.color = FIELD_COLOR\n input_parameters.font_color = { 0, 0, 0 }\n input_parameters.validation = 2 -- Integer\n\n -- Green\n input_parameters.input_function = \"greenDeckChanged\"\n input_parameters.position = { -0.166, 0.1, 0.385 }\n input_parameters.value = greenDeckId\n self.createInput(input_parameters)\n -- Red\n input_parameters.input_function = \"redDeckChanged\"\n input_parameters.position = { 0.171, 0.1, 0.385 }\n input_parameters.value = redDeckId\n self.createInput(input_parameters)\n -- White\n input_parameters.input_function = \"whiteDeckChanged\"\n input_parameters.position = { -0.166, 0.1, 0.474 }\n input_parameters.value = whiteDeckId\n self.createInput(input_parameters)\n -- Orange\n input_parameters.input_function = \"orangeDeckChanged\"\n input_parameters.position = { 0.171, 0.1, 0.474 }\n input_parameters.value = orangeDeckId\n self.createInput(input_parameters)\nend\n\n-- Create the Build All button. This is a transparent button which covers the Build All portion of the background graphic\nfunction makeBuildButton()\n local button_parameters = {}\n button_parameters.click_function = \"loadDecks\"\n button_parameters.function_owner = self\n button_parameters.position = { 0, 0.1, 0.71 }\n button_parameters.width = 320\n button_parameters.height = 30\n button_parameters.color = { 0, 0, 0, 0 }\n button_parameters.tooltip = \"Click to build all four decks!\"\n self.createButton(button_parameters)\nend\n\n-- Event handlers for deck ID change\nfunction redDeckChanged(_, _, inputValue) redDeckId = inputValue end\n\nfunction orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end\n\nfunction whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end\n\nfunction greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end\n\n-- Event handlers for toggle buttons\nfunction publicPrivateChanged()\n privateDeck = not privateDeck\n self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck] }\nend\n\nfunction loadUpgradedChanged()\n loadNewestDeck = not loadNewestDeck\n self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] }\nend\n\nfunction loadInvestigatorsChanged()\n loadInvestigators = not loadInvestigators\n self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] }\nend\n\nfunction loadDecks()\n -- testLoadLotsOfDecks()\n -- Method in DeckImporterMain, visible due to inclusion\n\n local indexReady = allCardsBagApi.isIndexReady()\n if (not indexReady) then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", {0.9, 0.2, 0.2})\n return\n end\n if (redDeckId ~= nil and redDeckId ~= \"\") then\n buildDeck(\"Red\", redDeckId)\n end\n if (orangeDeckId ~= nil and orangeDeckId ~= \"\") then\n buildDeck(\"Orange\", orangeDeckId)\n end\n if (whiteDeckId ~= nil and whiteDeckId ~= \"\") then\n buildDeck(\"White\", whiteDeckId)\n end\n if (greenDeckId ~= nil and greenDeckId ~= \"\") then\n buildDeck(\"Green\", greenDeckId)\n end\nend\nend)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id table String ID of the card to retrieve\n ---@return table table\n -- If the indexes are still being constructed, an empty table is\n -- returned. Otherwise, a single table with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", {id = id})\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned\n -- it will be removed from the list and cannot be selected again until a reload\n -- occurs or the indexes are rebuilt, which will refresh the list to include all\n -- weaknesses.\n ---@return string: ID of the selected weakness.\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n return getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list. Note that this is\n -- an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return getAllCardsBag().call(\"getCardsByName\", {name = name, exact = exact})\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean true for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return getAllCardsBag().call(\"getCardsByClassAndLevel\", {class = class, upgraded = upgraded})\n end\n\n AllCardsBagApi.getCardsByCycle = function(cycle)\n return getAllCardsBag().call(\"getCardsByCycle\", cycle)\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return getAllCardsBag().call(\"getUniqueWeaknesses\")\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"playercards/PlayerCardSpawner\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Amount to shift for the next card (zShift) or next row of cards (xShift)\n-- Note that the table rotation is weird, and the X axis is vertical while the\n-- Z axis is horizontal\nlocal SPREAD_Z_SHIFT = -2.3\nlocal SPREAD_X_SHIFT = -3.66\n\nSpawner = { }\n\n-- Spawns a list of cards at the given position/rotation. This will separate cards by size -\n-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If\n-- there are different types, the provided callback will be called once for each type as it spawns\n-- either a card or deck.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos tts__Vector table where the cards should be spawned (global)\n---@param rot tts__Vector table for the orientation of the spawned cards (global)\n---@param sort boolean True if this list of cards should be sorted before spawning\n---@param callback? function Callback to be called after the card/deck spawns.\nSpawner.spawnCards = function(cardList, pos, rot, sort, callback)\n if (sort) then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local miniCards = { }\n local standardCards = { }\n local investigatorCards = { }\n\n for _, card in ipairs(cardList) do\n if (card.metadata.type == \"Investigator\") then\n table.insert(investigatorCards, card)\n elseif (card.metadata.type == \"Minicard\") then\n table.insert(miniCards, card)\n else\n table.insert(standardCards, card)\n end\n end\n -- Spawn each of the three types individually. Each Y position shift accounts for the thickness\n -- of the spawned deck\n local position = { x = pos.x, y = pos.y, z = pos.z }\n Spawner.spawn(investigatorCards, position, rot, callback)\n\n position.y = position.y + (#investigatorCards + #standardCards) * 0.07\n Spawner.spawn(standardCards, position, rot, callback)\n\n position.y = position.y + (#standardCards + #miniCards) * 0.07\n Spawner.spawn(miniCards, position, rot, callback)\nend\n\nSpawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)\n if (sort) then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local position = { x = startPos.x, y = startPos.y, z = startPos.z }\n -- Special handle the first row if we have less than a full single row, but only if there's a\n -- reasonable max column count. Single-row spreads will send a large value for maxCols\n if maxCols \u003c 100 and #cardList \u003c maxCols then\n position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)\n end\n local cardsInRow = 0\n local rows = 0\n for _, card in ipairs(cardList) do\n Spawner.spawn({ card }, position, rot, callback)\n position.z = position.z + SPREAD_Z_SHIFT\n cardsInRow = cardsInRow + 1\n if cardsInRow \u003e= maxCols then\n rows = rows + 1\n local cardsForRow = #cardList - rows * maxCols\n if cardsForRow \u003e maxCols then\n cardsForRow = maxCols\n end\n position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)\n position.x = position.x + SPREAD_X_SHIFT\n cardsInRow = 0\n end\n end\nend\n\n-- Spawn a specific list of cards. This method is for internal use and should not be called\n-- directly, use spawnCards instead.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos table Position where the cards should be spawned (global)\n---@param rot table Rotation for the orientation of the spawned cards (global)\n---@param callback? function callback to be called after the card/deck spawns.\nSpawner.spawn = function(cardList, pos, rot, callback)\n if #cardList == 0 then return end\n\n -- Spawn a single card directly\n if #cardList == 1 then\n -- handle sideways card\n if cardList[1].data.SidewaysCard then\n rot = { rot.x, rot.y - 90, rot.z }\n end\n spawnObjectData({\n data = cardList[1].data,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\n return\n end\n\n -- For multiple cards, construct a deck and spawn that\n local deck = Spawner.buildDeckDataTemplate()\n\n -- Decks won't inherently scale to the cards in them. The card list being spawned should be all\n -- the same type/size by this point, so use the first card to set the size\n deck.Transform = {\n scaleX = cardList[1].data.Transform.scaleX,\n scaleY = 1,\n scaleZ = cardList[1].data.Transform.scaleZ\n }\n\n local sidewaysDeck = true\n for _, spawnCard in ipairs(cardList) do\n Spawner.addCardToDeck(deck, spawnCard.data)\n -- set sidewaysDeck to false if any card is not a sideways card\n sidewaysDeck = (sidewaysDeck and spawnCard.data.SidewaysCard)\n end\n\n -- set the alt view angle for sideways decks\n if sidewaysDeck then\n deck.AltLookAngle = { x = 0, y = 180, z = 90 }\n rot = { rot.x, rot.y - 90, rot.z }\n end\n\n spawnObjectData({\n data = deck,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\nend\n\n-- Inserts a card into the given deck. This does three things:\n-- 1. Add the card's data to ContainedObjects\n-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's\n-- ID list. Note that the deck's ID list is \"DeckIDs\" even though it\n-- contains a list of card Ids\n-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's\n-- \"CustomDeck\" field is a list of all CustomDecks used by cards within the\n-- deck, keyed by the DeckID and referencing the custom deck table\n---@param deck table TTS deck data structure to add to\n---@param cardData table Data for the card to be inserted\nSpawner.addCardToDeck = function(deck, cardData)\n for customDeckId, customDeckData in pairs(cardData.CustomDeck) do\n if (deck.CustomDeck[customDeckId] == nil) then\n -- CustomDeck not added to deck yet, add it\n deck.CustomDeck[customDeckId] = customDeckData\n elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then\n -- CustomDeck for this card matches the current one for the deck, do nothing\n else\n -- CustomDeck data conflict\n local newDeckId = nil\n for deckId, customDeck in pairs(deck.CustomDeck) do\n if (customDeckData.FaceURL == customDeck.FaceURL) then\n newDeckId = deckId\n end\n end\n if (newDeckId == nil) then\n -- No non-conflicting custom deck for this card, add a new one\n newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, \"1000\")\n deck.CustomDeck[newDeckId] = customDeckData\n end\n -- Update the card with the new CustomDeck info\n cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)\n cardData.CustomDeck[customDeckId] = nil\n cardData.CustomDeck[newDeckId] = customDeckData\n break\n end\n end\n table.insert(deck.ContainedObjects, cardData)\n table.insert(deck.DeckIDs, cardData.CardID)\nend\n\n-- Create an empty deck data table which can have cards added to it. This\n-- creates a new table on each call without using metatables or previous\n-- definitions because we can't be sure that TTS doesn't modify the structure\n---@return table deck Table containing the minimal TTS deck data structure\nSpawner.buildDeckDataTemplate = function()\n local deck = {}\n deck.Name = \"Deck\"\n\n -- Card data. DeckIDs and CustomDeck entries will be built from the cards\n deck.ContainedObjects = {}\n deck.DeckIDs = {}\n deck.CustomDeck = {}\n\n -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here\n deck.Transform = {\n scaleX = 1,\n scaleY = 1,\n scaleZ = 1,\n }\n\n return deck\nend\n\n-- Returns the first ID which does not exist in the given table, starting at startId and increasing\n---@param objectTable table keyed by strings which are numbers\n---@param startId string possible ID.\n---@return string id \u003e= startId\nSpawner.findNextAvailableId = function(objectTable, startId)\n local id = startId\n while (objectTable[id] ~= nil) do\n id = tostring(tonumber(id) + 1)\n end\n return id\nend\n\n-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.\n---@return number PBCN 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are\n-- irrelevant as they provide only grouping and the order between them doesn't matter.\nSpawner.getpbcn = function(metadata)\n if metadata.permanent then\n return 1\n elseif metadata.bonded_to ~= nil then\n return 2\n else -- Normal card\n return 3\n end\nend\n\n-- Comparison function used to sort the cards in a deck. Groups bonded or\n-- permanent cards first, then sorts within theose types by name/subname.\n-- Normal cards will sort in standard alphabetical order, while\n-- permanent/bonded/customizable will be in reverse alphabetical order.\n--\n-- Since cards spawn in the order provided by this comparator, with the first\n-- cards ending up at the bottom of a pile, this ordering will spawn in reverse\n-- alphabetical order. This presents the cards in order for non-face-down\n-- areas, and presents them in order when Searching the face-down deck.\nSpawner.cardComparator = function(card1, card2)\n local pbcn1 = Spawner.getpbcn(card1.metadata)\n local pbcn2 = Spawner.getpbcn(card2.metadata)\n if pbcn1 ~= pbcn2 then\n return pbcn1 \u003e pbcn2\n end\n if pbcn1 == 3 then\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n end\n return card1.data.Description \u003c card2.data.Description\n else\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003e card2.data.Nickname\n end\n return card1.data.Description \u003e card2.data.Description\n end\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"arkhamdb/DeckImporter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/PlayerCardSpawner\")\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\nlocal arkhamDb = require(\"arkhamdb/ArkhamDb\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal zones = require(\"playermat/Zones\")\n\nlocal matsWithInvestigator = {}\nlocal startsInPlayCount = 0\n\nlocal INPUT_FIELD_HEIGHT = 340\nlocal INPUT_FIELD_WIDTH = 1500\nlocal FIELD_COLOR = { 0.9, 0.7, 0.5 }\n\nlocal PRIVATE_TOGGLE_LABELS = {}\nPRIVATE_TOGGLE_LABELS[true] = \"Private\"\nPRIVATE_TOGGLE_LABELS[false] = \"Published\"\n\nlocal UPGRADED_TOGGLE_LABELS = {}\nUPGRADED_TOGGLE_LABELS[true] = \"Upgraded\"\nUPGRADED_TOGGLE_LABELS[false] = \"Specific\"\n\nlocal LOAD_INVESTIGATOR_TOGGLE_LABELS = {}\nLOAD_INVESTIGATOR_TOGGLE_LABELS[true] = \"Yes\"\nLOAD_INVESTIGATOR_TOGGLE_LABELS[false] = \"No\"\n\nlocal redDeckId = \"\"\nlocal orangeDeckId = \"\"\nlocal whiteDeckId = \"\"\nlocal greenDeckId = \"\"\n\nlocal privateDeck = true\nlocal loadNewestDeck = true\nlocal loadInvestigators = false\n\nfunction onLoad(script_state)\n initializeUi(JSON.decode(script_state))\n math.randomseed(os.time())\n arkhamDb.initialize()\nend\n\nfunction onSave() return JSON.encode(getUiState()) end\n\n-- Returns a table with the full state of the UI, including options and deck IDs.\n-- This can be used to persist via onSave(), or provide values for a load operation\n---@return uiStateTable uiStateTable Contains data about the current UI state\nfunction getUiState()\n return {\n redDeck = redDeckId,\n orangeDeck = orangeDeckId,\n whiteDeck = whiteDeckId,\n greenDeck = greenDeckId,\n privateDeck = privateDeck,\n loadNewest = loadNewestDeck,\n investigators = loadInvestigators\n }\nend\n\n-- Updates the state of the UI based on the provided table. Any values not provided will be left the same.\n---@param uiStateTable table Table of values to update on importer\nfunction setUiState(uiStateTable)\n self.clearButtons()\n self.clearInputs()\n initializeUi(uiStateTable)\nend\n\n-- Sets up the UI for the deck loader, populating fields from the given save state table decoded from onLoad()\nfunction initializeUi(savedUiState)\n if savedUiState ~= nil then\n redDeckId = savedUiState.redDeck\n orangeDeckId = savedUiState.orangeDeck\n whiteDeckId = savedUiState.whiteDeck\n greenDeckId = savedUiState.greenDeck\n privateDeck = savedUiState.privateDeck\n loadNewestDeck = savedUiState.loadNewest\n loadInvestigators = savedUiState.investigators\n end\n\n makeOptionToggles()\n makeDeckIdFields()\n makeBuildButton()\nend\n\nfunction makeOptionToggles()\n -- common parameters\n local checkbox_parameters = {}\n checkbox_parameters.function_owner = self\n checkbox_parameters.width = INPUT_FIELD_WIDTH\n checkbox_parameters.height = INPUT_FIELD_HEIGHT\n checkbox_parameters.scale = { 0.1, 0.1, 0.1 }\n checkbox_parameters.font_size = 240\n checkbox_parameters.hover_color = { 0.4, 0.6, 0.8 }\n checkbox_parameters.color = FIELD_COLOR\n\n -- public / private deck\n checkbox_parameters.click_function = \"publicPrivateChanged\"\n checkbox_parameters.position = { 0.25, 0.1, -0.102 }\n checkbox_parameters.tooltip = \"Published or private deck?\\n\\nPLEASE USE A PRIVATE DECK IF JUST FOR TTS TO AVOID FLOODING ARKHAMDB PUBLISHED DECK LISTS!\"\n checkbox_parameters.label = PRIVATE_TOGGLE_LABELS[privateDeck]\n self.createButton(checkbox_parameters)\n\n -- load upgraded?\n checkbox_parameters.click_function = \"loadUpgradedChanged\"\n checkbox_parameters.position = { 0.25, 0.1, -0.01 }\n checkbox_parameters.tooltip = \"Load newest upgrade or exact deck?\"\n checkbox_parameters.label = UPGRADED_TOGGLE_LABELS[loadNewestDeck]\n self.createButton(checkbox_parameters)\n\n -- load investigators?\n checkbox_parameters.click_function = \"loadInvestigatorsChanged\"\n checkbox_parameters.position = { 0.25, 0.1, 0.081 }\n checkbox_parameters.tooltip = \"Spawn investigator cards?\"\n checkbox_parameters.label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators]\n self.createButton(checkbox_parameters)\nend\n\n-- Create the four deck ID entry fields\nfunction makeDeckIdFields()\n local input_parameters = {}\n -- Parameters common to all entry fields\n input_parameters.function_owner = self\n input_parameters.scale = { 0.1, 0.1, 0.1 }\n input_parameters.width = INPUT_FIELD_WIDTH\n input_parameters.height = INPUT_FIELD_HEIGHT\n input_parameters.font_size = 320\n input_parameters.tooltip = \"Deck ID from ArkhamDB URL of the deck\\nPublic URL: 'https://arkhamdb.com/decklist/view/101/knowledge-overwhelming-solo-deck-1.0' = '101'\\nPrivate URL: 'https://arkhamdb.com/deck/view/102' = '102'\"\n input_parameters.alignment = 3 -- Center\n input_parameters.color = FIELD_COLOR\n input_parameters.font_color = { 0, 0, 0 }\n input_parameters.validation = 2 -- Integer\n\n -- Green\n input_parameters.input_function = \"greenDeckChanged\"\n input_parameters.position = { -0.166, 0.1, 0.385 }\n input_parameters.value = greenDeckId\n self.createInput(input_parameters)\n -- Red\n input_parameters.input_function = \"redDeckChanged\"\n input_parameters.position = { 0.171, 0.1, 0.385 }\n input_parameters.value = redDeckId\n self.createInput(input_parameters)\n -- White\n input_parameters.input_function = \"whiteDeckChanged\"\n input_parameters.position = { -0.166, 0.1, 0.474 }\n input_parameters.value = whiteDeckId\n self.createInput(input_parameters)\n -- Orange\n input_parameters.input_function = \"orangeDeckChanged\"\n input_parameters.position = { 0.171, 0.1, 0.474 }\n input_parameters.value = orangeDeckId\n self.createInput(input_parameters)\nend\n\n-- Create the Build All button. This is a transparent button which covers the Build All portion of the background graphic\nfunction makeBuildButton()\n local button_parameters = {}\n button_parameters.click_function = \"loadDecks\"\n button_parameters.function_owner = self\n button_parameters.position = { 0, 0.1, 0.71 }\n button_parameters.width = 320\n button_parameters.height = 30\n button_parameters.color = { 0, 0, 0, 0 }\n button_parameters.tooltip = \"Click to build all four decks!\"\n self.createButton(button_parameters)\nend\n\n-- Event handlers for deck ID change\nfunction redDeckChanged(_, _, inputValue) redDeckId = inputValue end\nfunction orangeDeckChanged(_, _, inputValue) orangeDeckId = inputValue end\nfunction whiteDeckChanged(_, _, inputValue) whiteDeckId = inputValue end\nfunction greenDeckChanged(_, _, inputValue) greenDeckId = inputValue end\n\n-- Event handlers for toggle buttons\nfunction publicPrivateChanged()\n privateDeck = not privateDeck\n self.editButton { index = 0, label = PRIVATE_TOGGLE_LABELS[privateDeck] }\nend\n\nfunction loadUpgradedChanged()\n loadNewestDeck = not loadNewestDeck\n self.editButton { index = 1, label = UPGRADED_TOGGLE_LABELS[loadNewestDeck] }\nend\n\nfunction loadInvestigatorsChanged()\n loadInvestigators = not loadInvestigators\n self.editButton { index = 2, label = LOAD_INVESTIGATOR_TOGGLE_LABELS[loadInvestigators] }\nend\n\nfunction loadDecks()\n if not allCardsBagApi.isIndexReady() then return end\n matsWithInvestigator = playermatApi.getUsedMatColors()\n if redDeckId ~= nil and redDeckId ~= \"\" then\n buildDeck(\"Red\", redDeckId)\n end\n if orangeDeckId ~= nil and orangeDeckId ~= \"\" then\n buildDeck(\"Orange\", orangeDeckId)\n end\n if whiteDeckId ~= nil and whiteDeckId ~= \"\" then\n buildDeck(\"White\", whiteDeckId)\n end\n if greenDeckId ~= nil and greenDeckId ~= \"\" then\n buildDeck(\"Green\", greenDeckId)\n end\nend\n\n-- Returns the zone name where the specified card should be placed, based on its metadata.\n---@param cardMetadata table Contains card metadata\n---@return string Zone Name of the zone such as \"Deck\", \"SetAside1\", etc. (See zones file for a list of valid zones)\nfunction getDefaultCardZone(cardMetadata, bondedList)\n if cardMetadata.id == \"09080-m\" then -- Have to check the Servitor before other minicards\n return \"SetAside6\"\n elseif cardMetadata.id == \"09006\" then -- On The Mend is set aside\n return \"SetAside2\"\n elseif bondedList[cardMetadata.id] then\n return \"SetAside2\"\n elseif cardMetadata.type == \"Investigator\" then\n return \"Investigator\"\n elseif cardMetadata.type == \"Minicard\" then\n return \"Minicard\"\n elseif cardMetadata.type == \"UpgradeSheet\" then\n return \"SetAside4\"\n elseif cardMetadata.startsInPlay then\n return startsInPlayTracker()\n elseif cardMetadata.permanent then\n return \"SetAside1\"\n -- SetAside3 is used for Ancestral Knowledge / Underworld Market\n else\n return \"Deck\"\n end\nend\n\nfunction startsInPlayTracker()\n startsInPlayCount = startsInPlayCount + 1\n if startsInPlayCount \u003e 6 then\n broadcastToAll(\"Card that should start in play was placed with permanents because no blank slots remained\")\n return \"SetAside1\"\n else\n return \"Blank\" .. startsInPlayCount\n end\nend\n\nfunction buildDeck(playerColor, deckId)\n local uiState = getUiState()\n arkhamDb.getDecklist(\n playerColor,\n deckId,\n uiState.privateDeck,\n uiState.loadNewest,\n uiState.investigators,\n loadCards)\nend\n\n-- Process the slot list, which defines the card Ids and counts of cards to load. Spawn those cards\n-- at the appropriate zones and report an error to the user if any could not be loaded.\n-- This is a callback function which handles the results of ArkhamDb.getDecklist()\n-- This method uses an encapsulated coroutine with yields to make the card spawning cleaner.\n---@param slots table Key-Value table of cardId:count\n---@param investigatorId string ArkhamDB ID (code) for this deck's investigator.\n-- Investigator cards should already be added to the slots list if they should be spawned,\n-- but this value is separate to check for special handling for certain investigators\n---@param bondedList table A table of cardID keys to meaningless values. Card IDs in this list were added\n-- from a parent bonded card.\n---@param customizations table ArkhamDB data for customizations on customizable cards\n---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\")\n---@param loadAltInvestigator string Contains the name of alternative art for the investigator (\"normal\", \"revised\" or \"promo\")\nfunction loadCards(slots, investigatorId, bondedList, customizations, playerColor, loadAltInvestigator)\n function coinside()\n local cardsToSpawn = {}\n local resourceModifier = 0\n\n -- reset the startsInPlayCount\n startsInPlayCount = 0\n for cardId, cardCount in pairs(slots) do\n local card = allCardsBagApi.getCardById(cardId)\n if card ~= nil then\n local cardZone = getDefaultCardZone(card.metadata, bondedList)\n for i = 1, cardCount do\n table.insert(cardsToSpawn, { data = card.data, metadata = card.metadata, zone = cardZone })\n end\n slots[cardId] = 0\n end\n\n -- check for resource modifiers\n if cardId == \"02037\" then -- Indebted\n resourceModifier = resourceModifier - 2 * cardCount\n elseif cardId == \"05278\" then -- Another Day, Another Dollar\n resourceModifier = resourceModifier + 2 * cardCount\n end\n end\n\n updateStartingResources(playerColor, resourceModifier)\n handleAncestralKnowledge(cardsToSpawn)\n handleUnderworldMarket(cardsToSpawn, playerColor)\n handleHunchDeck(investigatorId, cardsToSpawn, bondedList, playerColor)\n handleSpiritDeck(investigatorId, cardsToSpawn, playerColor, customizations)\n handleCustomizableUpgrades(cardsToSpawn, customizations)\n handlePeteSignatureAssets(investigatorId, cardsToSpawn)\n\n -- Split the card list into separate lists for each zone\n local zoneDecks = buildZoneLists(cardsToSpawn)\n\n -- Check for existing cards in zones and maybe skip them\n removeBusyZones(playerColor, zoneDecks)\n\n -- Spawn the list for each zone\n for zone, zoneCards in pairs(zoneDecks) do\n local deckPos = zones.getZonePosition(playerColor, zone):setAt(\"y\", 3)\n local deckRot = zones.getDefaultCardRotation(playerColor, zone)\n local callback = nil\n\n -- If cards are spread too close together TTS groups them weirdly, selecting multiples\n -- when hovering over a single card. This distance is the minimum to avoid that.\n local spreadDistance = 1.15\n if (zone == \"SetAside4\") then\n -- SetAside4 is reserved for customization cards, and we want them spread on the table\n -- so their checkboxes are visible\n -- TO-DO: take into account that spreading will make multiple rows\n -- (this is affected by the user's local settings!)\n if (playerColor == \"White\") then\n deckPos.z = deckPos.z + (#zoneCards - 1) * spreadDistance\n elseif (playerColor == \"Green\") then\n deckPos.x = deckPos.x + (#zoneCards - 1) * spreadDistance\n end\n callback = function(deck) deck.spread(spreadDistance) end\n elseif zone == \"Deck\" then\n callback = function(deck) deckSpawned(deck, playerColor) end\n elseif zone == \"Investigator\" or zone == \"Minicard\" then\n callback = function(card) loadAltArt(card, loadAltInvestigator) end\n end\n Spawner.spawnCards(zoneCards, deckPos, deckRot, true, callback)\n coroutine.yield(0)\n end\n\n -- Look for any cards which haven't been loaded\n local hadError = false\n for cardId, remainingCount in pairs(slots) do\n if remainingCount \u003e 0 then\n hadError = true\n arkhamDb.logCardNotFound(cardId, playerColor)\n end\n end\n if (not hadError) then\n printToAll(\"Deck loaded successfully!\", playerColor)\n end\n return 1\n end\n\n startLuaCoroutine(self, \"coinside\")\nend\n\n-- Callback handler for the main deck spawning. Looks for cards which should start in hand, and\n-- draws them for the appropriate player.\n---@param deck tts__Object Callback-provided spawned deck object\n---@param playerColor string Color of the player to draw the cards to\nfunction deckSpawned(deck, playerColor)\n local player = Player[playermatApi.getPlayerColor(playerColor)]\n local handPos = player.getHandTransform(1).position -- Only one hand zone per player\n local deckCards = deck.getData().ContainedObjects\n\n -- Process in reverse order so taking cards out doesn't upset the indexing\n for i = #deckCards, 1, -1 do\n local cardMetadata = JSON.decode(deckCards[i].GMNotes) or { }\n if cardMetadata.startsInHand then\n deck.takeObject({ index = i - 1, position = handPos, flip = true, smooth = true})\n end\n end\n\n -- add the \"PlayerCard\" tag to the deck\n if deck and deck.type == \"Deck\" and deck.getQuantity() \u003e 1 then\n deck.addTag(\"PlayerCard\")\n end\nend\n\n-- Converts the Raven Quill's selections from card IDs to card names. This could be more elegant\n-- but the inputs are very static so we're using some brute force.\n---@param selectionString string provided by ArkhamDB, indicates the customization selections\n-- Should be either a single card ID or two separated by a ^ (e.g. XXXXX^YYYYY)\nfunction convertRavenQuillSelections(selectionString)\n if string.len(selectionString) == 5 then\n return getCardName(selectionString)\n elseif string.len(selectionString) == 11 then\n return getCardName(string.sub(selectionString, 1, 5)) .. \", \" .. getCardName(string.sub(selectionString, 7))\n end\nend\n\n-- Converts Grizzled's selections from a single string with \"^\".\n---@param selectionString string provided by ArkhamDB, indicates the customization selections\n-- Should be two traits separated by a ^ (e.g. XXXXX^YYYYY)\nfunction convertGrizzledSelections(selectionString)\n return selectionString:gsub(\"%^\", \", \")\nend\n\n-- Returns the simple name of a card given its ID. This will find the card and strip any trailing\n-- SCED-specific suffixes such as (Taboo) or (Level)\nfunction getCardName(cardId)\n local card = allCardsBagApi.getCardById(cardId)\n if card then\n local name = card.data.Nickname\n if string.find(name, \" %(\") then\n return string.sub(name, 1, string.find(name, \" %(\") - 1)\n else\n return name\n end\n end\nend\n\n-- Split a single list of cards into a separate table of lists, keyed by the zone\n---@param cards table Table of {cardData, cardMetadata, zone}\n---@return table zoneDecks Table with zoneName as index: {zoneName=card list}\nfunction buildZoneLists(cards)\n local zoneList = {}\n for _, card in ipairs(cards) do\n if zoneList[card.zone] == nil then\n zoneList[card.zone] = {}\n end\n table.insert(zoneList[card.zone], card)\n end\n\n return zoneList\nend\n\n-- removes zones from list if they are occupied\n---@param playerColor string Color this deck is being loaded for\n---@param zoneDecks table Table with zoneName as index: {zoneName=card list}\nfunction removeBusyZones(playerColor, zoneDecks)\n -- check for existing investigator\n for _, matColor in ipairs(matsWithInvestigator) do\n if matColor == playerColor then\n zoneDecks[\"Investigator\"] = nil\n printToAll(\"Skipped investigator import\", playerColor)\n break\n end\n end\n\n -- check for existing minicard\n local mat = guidReferenceApi.getObjectByOwnerAndType(playerColor, \"Playermat\")\n local miniId = mat.getVar(\"activeInvestigatorId\") .. \"-m\"\n\n -- remove taboo suffix since we don't have this for minicards\n miniId = miniId:gsub(\"-t\", \"\")\n\n for _, obj in ipairs(getObjectsWithTag(\"Minicard\")) do\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.id == miniId then\n local pos = mat.positionToWorld(Vector(-1.36, 0, -0.625)):setAt(\"y\", 1.67)\n obj.setPosition(pos)\n zoneDecks[\"Minicard\"] = nil\n printToAll(\"Skipped minicard import\", playerColor)\n break\n end\n end\n\n -- check for existing deck\n local cardsInDeckArea = 0\n for _, obj in pairs(playermatApi.getDeckAreaObjects(playerColor)) do\n cardsInDeckArea = cardsInDeckArea + #obj.getObjects()\n end\n\n -- threshhold of 16 cards for skipping deck import to cover cases like Tekeli-li cards\n if cardsInDeckArea \u003e 16 then\n for i = 1, 6 do\n zoneDecks[\"SetAside\" .. i] = nil\n zoneDecks[\"Blank\" .. i] = nil\n end\n zoneDecks[\"Deck\"] = nil\n printToAll(\"Skipped deck import\", playerColor)\n end\nend\n\n-- Check to see if the deck list has Ancestral Knowledge. If it does, move 5 random skills to SetAside3\n---@param cardList table Deck list being created\nfunction handleAncestralKnowledge(cardList)\n local hasAncestralKnowledge = false\n local skillList = {}\n\n -- Have to process the entire list to check for Ancestral Knowledge and get all possible skills, so do both in one pass\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"07303\" then\n hasAncestralKnowledge = true\n card.zone = \"SetAside3\"\n elseif (card.metadata.type == \"Skill\"\n and card.zone == \"Deck\"\n and not card.metadata.weakness) then\n table.insert(skillList, i)\n end\n end\n\n if not hasAncestralKnowledge then return end\n\n -- Move 5 random skills to SetAside3\n for i = 1, 5 do\n local skillListIndex = math.random(#skillList)\n cardList[skillList[skillListIndex]].zone = \"UnderSetAside3\"\n table.remove(skillList, skillListIndex)\n end\nend\n\n-- Check for and handle Underworld Market by moving all Illicit cards to UnderSetAside3\n---@param cardList table Deck list being created\n---@param playerColor string Color this deck is being loaded for\nfunction handleUnderworldMarket(cardList, playerColor)\n local hasMarket = false\n local illicitList = {}\n\n -- Process the entire list to check for Underworld Market and get all possible Illicit cards, doing both in one pass\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"09077\" then\n hasMarket = true\n card.zone = \"SetAside3\"\n elseif card.metadata.traits ~= nil and string.find(card.metadata.traits, \"Illicit\", 1, true) and card.zone == \"Deck\" then\n table.insert(illicitList, i)\n end\n end\n\n if not hasMarket then return end\n\n if #illicitList \u003c 10 then\n printToAll(\"Only \" .. #illicitList .. \" Illicit cards in your deck, you can't trigger Underworld Market's ability.\", playerColor)\n else\n -- Process cards to move them to the market deck. This is done in reverse order because the sorting needs\n -- to be reversed (deck sorts for face down). Performance here may be an issue, as table.remove() is an O(n)\n -- operation which makes the full shift O(n^2). But keep it simple unless it becomes a problem\n for i = #illicitList, 1, -1 do\n local moving = cardList[illicitList[i]]\n moving.zone = \"UnderSetAside3\"\n table.remove(cardList, illicitList[i])\n table.insert(cardList, moving)\n end\n\n if #illicitList \u003e 10 then\n printToAll(\"Moved all \" .. #illicitList .. \" Illicit cards to the Market deck, reduce it to 10\", playerColor)\n else\n printToAll(\"Built the Market deck\", playerColor)\n end\n end\nend\n\n-- If the investigator is Joe Diamond, extract all Insight events to SetAside5 to build the Hunch Deck\n---@param investigatorId string ID for the deck's investigator card\n---@param cardList table Deck list being created\n---@param playerColor string Color this deck is being loaded for\nfunction handleHunchDeck(investigatorId, cardList, bondedList, playerColor)\n if investigatorId ~= \"05002\" then return end\n\n local insightList = {}\n for i, card in ipairs(cardList) do\n if (card.metadata.type == \"Event\"\n and card.metadata.traits ~= nil\n and string.match(card.metadata.traits, \"Insight\")\n and bondedList[card.metadata.id] == nil) then\n table.insert(insightList, i)\n end\n end\n -- Process cards to move them to the hunch deck. This is done in reverse order because the sorting needs\n -- to be reversed (deck sorts for face down). Performance here may be an issue, as table.remove() is an O(n)\n -- operation which makes the full shift O(n^2). But keep it simple unless it becomes a problem\n for i = #insightList, 1, -1 do\n local moving = cardList[insightList[i]]\n moving.zone = \"SetAside5\"\n table.remove(cardList, insightList[i])\n table.insert(cardList, moving)\n end\n\n if #insightList \u003c 11 then\n printToAll(\"Joe's hunch deck must have 11 cards but the deck only has \" .. #insightList .. \" Insight events.\", playerColor)\n elseif #insightList \u003e 11 then\n printToAll(\"Moved all \" .. #insightList .. \" Insight events to the hunch deck, reduce it to 11.\", playerColor)\n else\n printToAll(\"Built Joe's hunch deck\", playerColor)\n end\nend\n\n-- If the investigator is Parallel Jim Culver, extract all Ally assets to SetAside5 to build the Spirit Deck\n---@param investigatorId string ID for the deck's investigator card\n---@param cardList table Deck list being created\n---@param playerColor string Color this deck is being loaded for\n---@param customizations table Additional deck information\nfunction handleSpiritDeck(investigatorId, cardList, playerColor, customizations)\n if investigatorId ~= \"02004-p\" and investigatorId ~= \"02004-pb\" then return end\n\n local spiritList = {}\n if customizations[\"extra_deck\"] then\n -- split by \",\"\n for str in string.gmatch(customizations[\"extra_deck\"], \"([^,]+)\") do\n local card = allCardsBagApi.getCardById(str)\n if card ~= nil then\n table.insert(cardList, { data = card.data, metadata = card.metadata, zone = \"SetAside5\" })\n table.insert(spiritList, str)\n end\n end\n else\n for i, card in ipairs(cardList) do\n if card.metadata.id == \"90053\" or (\n card.metadata.type == \"Asset\"\n and card.metadata.traits ~= nil\n and string.match(card.metadata.traits, \"Ally\")\n and card.metadata.level ~= nil\n and card.metadata.level \u003c 3) then\n table.insert(spiritList, i)\n end\n end\n\n -- Process cards to move them to the spirit deck. This is done in reverse order because the sorting needs\n -- to be reversed (deck sorts for face down). Performance here may be an issue, as table.remove() is an O(n)\n -- operation which makes the full shift O(n^2). But keep it simple unless it becomes a problem\n for i = #spiritList, 1, -1 do\n local moving = cardList[spiritList[i]]\n moving.zone = \"SetAside5\"\n table.remove(cardList, spiritList[i])\n table.insert(cardList, moving)\n end\n end\n\n if #spiritList \u003c 10 then\n printToAll(\"Jim's spirit deck must have 9 Ally assets but the deck only has \" .. (#spiritList - 1) .. \" Ally assets.\", playerColor)\n elseif #spiritList \u003e 11 then\n printToAll(\"Moved all \" .. (#spiritList - 1) .. \" Ally assets to the spirit deck, reduce it to 10 (including Vengeful Shade).\", playerColor)\n else\n printToAll(\"Built Jim's spirit deck\", playerColor)\n end\nend\n\n-- For any customization upgrade cards in the card list, process the metadata from the deck to\n-- set the save state to show the correct checkboxes/text field values\n---@param cardList table Deck list being created\n---@param customizations table ArkhamDB data for customizations on customizable cards\nfunction handleCustomizableUpgrades(cardList, customizations)\n for _, card in ipairs(cardList) do\n if card.metadata.type == \"UpgradeSheet\" then\n local baseId = string.sub(card.metadata.id, 1, 5)\n local upgrades = customizations[\"cus_\" .. baseId]\n\n if upgrades ~= nil then\n -- contains the amount of markedBoxes (left to right) per row (starting at row 1)\n local selectedUpgrades = {}\n\n -- contains the amount of inputValues per row (starting at row 0)\n local index_xp = {}\n\n -- get the index and xp values (looks like this: X|X,X|X, ..)\n -- input string from ArkhamDB is split by \",\"\n for str in string.gmatch(customizations[\"cus_\" .. baseId], \"([^,]+)\") do\n table.insert(index_xp, str)\n end\n\n -- split each pair and assign it to the proper position in markedBoxes\n for _, entry in ipairs(index_xp) do\n -- counter increments from 1 to 3 and indicates the part of the string we are on\n -- usually: 1 = row, 2 = amount of check boxes, 3 = entry in inputfield\n local counter = 0\n local row = 0\n\n -- parsing the string for each row\n for str in entry:gmatch(\"([^|]+)\") do\n counter = counter + 1\n\n if counter == 1 then\n row = tonumber(str) + 1\n elseif counter == 2 then\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n end\n selectedUpgrades[row].xp = tonumber(str)\n elseif counter == 3 and str ~= \"\" then\n if baseId == \"09042\" then\n selectedUpgrades[row].text = convertRavenQuillSelections(str)\n elseif baseId == \"09101\" then\n selectedUpgrades[row].text = convertGrizzledSelections(str)\n elseif baseId == \"09079\" then -- Living Ink skill selection\n -- All skills, regardless of row, are placed in upgrade slot 1 as a comma-delimited list\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n if selectedUpgrades[1].text == nil then\n selectedUpgrades[1].text = str\n else\n selectedUpgrades[1].text = selectedUpgrades[1].text .. \",\" .. str\n end\n else\n selectedUpgrades[row].text = str\n end\n end\n end\n end\n\n -- write the loaded values to the save_data of the sheets\n card.data[\"LuaScriptState\"] = JSON.encode({ selections = selectedUpgrades })\n end\n end\n end\nend\n\n-- Handles cards that start in play under specific conditions for Ashcan Pete (Regular Pete - Duke, Parallel Pete - Guitar)\n---@param investigatorId string ID for the deck's investigator card\n---@param cardList table Deck list being created\nfunction handlePeteSignatureAssets(investigatorId, cardList)\n if investigatorId == \"02005\" or investigatorId == \"02005-pb\" then -- regular Pete's front\n for _, card in ipairs(cardList) do\n if card.metadata.id == \"02014\" then -- Duke\n card.zone = startsInPlayTracker()\n end\n end\n elseif investigatorId == \"02005-p\" or investigatorId == \"02005-pf\" then -- parallel Pete's front\n for _, card in ipairs(cardList) do\n if card.metadata.id == \"90047\" then -- Pete's Guitar\n card.zone = startsInPlayTracker()\n end\n end\n end\nend\n\n-- Callback function for investigator cards and minicards to set the correct state for alt art\n---@param card tts__Object Card which needs to be set the state for\n---@param loadAltInvestigator string Contains the name of alternative art for the investigator (\"normal\", \"revised\" or \"promo\")\nfunction loadAltArt(card, loadAltInvestigator)\n -- states are set up this way:\n -- 1 - normal, 2 - revised/promo, 3 - promo (if 2 is revised)\n -- This means we can always load the 2nd state for revised and just get the last state for promo\n if loadAltInvestigator == \"normal\" then\n return\n elseif loadAltInvestigator == \"revised\" then\n card.setState(2)\n elseif loadAltInvestigator == \"promo\" then\n local states = card.getStates()\n card.setState(#states)\n end\nend\n\n-- updates the starting resources\n---@param playerColor string Color this deck is being loaded for\n---@param resourceModifier number Modifier for the starting resources\nfunction updateStartingResources(playerColor, resourceModifier)\n if resourceModifier ~= 0 then\n playermatApi.updateCounter(playerColor, \"ResourceCounter\", _, resourceModifier)\n printToAll(\"Modified starting resources\", playerColor)\n end\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- internal function to create a copy of the table to avoid operating on variables owned by different objects\n local function returnCopyOfList(data)\n local copiedList = {}\n for _, id in ipairs(data) do\n table.insert(copiedList, id)\n end\n return copiedList\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id string ID of the card to retrieve\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a single table with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", { id = id })\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned it\n -- will be removed from the list and cannot be selected again until a reload occurs\n -- or the indexes are rebuilt, which will refresh the list to include all weaknesses.\n ---@return string: ID of the selected weakness\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list.\n -- Note that this is an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByName\", { name = name, exact = exact }))\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByClassAndLevel\", { class = class, upgraded = upgraded }))\n end\n\n -- Returns a list of cards from the bag matching a cycle\n ---@param cycle string Cycle to retrieve (\"The Scarlet Keys\" etc.)\n ---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByCycle\", { cycle = cycle, sortByMetadata = sortByMetadata }))\n end\n\n -- Constructs a list of available basic weaknesses by starting with the full pool of basic\n -- weaknesses then removing any which are currently in the play or deck construction areas\n ---@param traits? string Trait(s) to use as filter\n ---@return table: Array of weakness IDs which are valid to choose from\n AllCardsBagApi.buildAvailableWeaknesses = function(traits)\n return returnCopyOfList(getAllCardsBag().call(\"buildAvailableWeaknesses\", traits))\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return returnCopyOfList(getAllCardsBag().call(\"getUniqueWeaknesses\"))\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"arkhamdb/DeckImporter\")\nend)\n__bundle_register(\"arkhamdb/ArkhamDb\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n\n local ArkhamDb = {}\n local internal = {}\n \n local tabooList = {}\n local configuration\n\n local RANDOM_WEAKNESS_ID = \"01000\"\n\n ---@class Request\n local Request = {}\n\n -- Sets up the ArkhamDb interface. Should be called from the parent object on load.\n ArkhamDb.initialize = function()\n configuration = internal.getConfiguration()\n Request.start({ configuration.api_uri, configuration.taboo }, function(status)\n local json = JSON.decode(internal.fixUtf16String(status.text))\n for _, taboo in pairs(json) do\n local cards = {}\n\n for _, card in pairs(JSON.decode(taboo.cards)) do\n cards[card.code] = true\n end\n\n tabooList[taboo.id] = {\n date = taboo.date_start,\n cards = cards\n }\n end\n return true, nil\n end)\n end\n\n -- Start the deck build process for the given player color and deck ID. This\n -- will retrieve the deck from ArkhamDB, and pass to a callback for processing.\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\").\n ---@param deckId string ArkhamDB deck id to be loaded\n ---@param isPrivate boolean Whether this deck is published or private on ArkhamDB\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n ---@return boolean\n ---@return string\n ArkhamDb.getDecklist = function(\n playerColor,\n deckId,\n isPrivate,\n loadNewest,\n loadInvestigators,\n callback)\n -- Get a simple card to see if the bag indexes are complete. If not, abort\n -- the deck load. The called method will handle player notification.\n local checkCard = allCardsBagApi.getCardById(\"01001\")\n if (checkCard ~= nil and checkCard.data == nil) then\n return false, \"Indexing not complete\"\n end\n\n local deckUri = {\n configuration.api_uri,\n isPrivate and configuration.private_deck or configuration.public_deck,\n deckId\n }\n\n local deck = Request.start(deckUri, function(status)\n if string.find(status.text, \"\u003c!DOCTYPE html\u003e\") then\n internal.maybePrint(\"Private deck ID \" .. deckId .. \" is not shared.\", playerColor)\n return false, \"Private deck \" .. deckId .. \" is not shared\"\n end\n\n local json = JSON.decode(internal.fixUtf16String(status.text))\n\n if not json then\n internal.maybePrint(\"Deck ID \" .. deckId .. \" not found.\", playerColor)\n return false, \"Deck not found!\"\n end\n\n return true, json\n end)\n\n deck:with(internal.onDeckResult, playerColor, loadNewest, loadInvestigators, callback)\n end\n\n -- Logs that a card could not be loaded in the mod by printing it to the console in the given\n -- color of the player owning the deck. Attempts to look up the name on ArkhamDB for clarity,\n -- but prints the card ID if the name cannot be retrieved.\n ---@param cardId string ArkhamDB ID of the card that could not be found\n ---@param playerColor string Color of the player's deck that had the problem\n ArkhamDb.logCardNotFound = function(cardId, playerColor)\n local request = Request.start({\n configuration.api_uri,\n configuration.cards,\n cardId\n },\n function(result)\n local adbCardInfo = JSON.decode(internal.fixUtf16String(result.text))\n local cardName = adbCardInfo.real_name\n if (cardName ~= nil) then\n if (adbCardInfo.xp ~= nil and adbCardInfo.xp \u003e 0) then\n cardName = cardName .. \" (\" .. adbCardInfo.xp .. \")\"\n end\n internal.maybePrint(\"Card not found: \" .. cardName .. \", card ID \" .. cardId, playerColor)\n else\n internal.maybePrint(\"Card not found in ArkhamDB/Index, ID \" .. cardId, playerColor)\n end\n end)\n end\n\n -- Callback when the deck information is received from ArkhamDB. Parses the\n -- response then applies standard transformations to the deck such as adding\n -- random weaknesses and checking for taboos. Once the deck is processed,\n -- passes to loadCards to actually spawn the defined deck.\n ---@param deck table ArkhamImportDeck\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\")\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load.\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- bondedList A table of cardID keys to meaningless values. Card IDs in this list were\n --- added from a parent bonded card.\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n internal.onDeckResult = function(deck, playerColor, loadNewest, loadInvestigators, callback)\n -- Load the next deck in the upgrade path if the option is enabled\n if (loadNewest and deck.next_deck ~= nil and deck.next_deck ~= \"\") then\n buildDeck(playerColor, deck.next_deck)\n return\n end\n\n internal.maybePrint(table.concat({ \"Found decklist: \", deck.name }), playerColor)\n\n -- Initialize deck slot table and perform common transformations. The order of these should not\n -- be changed, as later steps may act on cards added in each. For example, a random weakness or\n -- investigator may have bonded cards or taboo entries, and should be present\n local slots = deck.slots\n internal.maybeDrawRandomWeakness(slots, playerColor)\n\n -- handles alternative investigators (parallel, promo or revised art)\n local loadAltInvestigator = \"normal\"\n if loadInvestigators then\n loadAltInvestigator = internal.addInvestigatorCards(deck, slots)\n end\n\n internal.maybeModifyDeckFromDescription(slots, deck.description_md, playerColor)\n internal.maybeAddSummonedServitor(slots)\n internal.maybeAddOnTheMend(slots, playerColor)\n internal.maybeAddRealityAcidReference(slots)\n local bondList = internal.extractBondedCards(slots)\n internal.checkTaboos(deck.taboo_id, slots, playerColor)\n internal.maybeAddUpgradeSheets(slots)\n\n -- get upgrades for customizable cards\n local customizations = {}\n if deck.meta then\n customizations = JSON.decode(deck.meta)\n end\n\n callback(slots, deck.investigator_code, bondList, customizations, playerColor, loadAltInvestigator)\n end\n\n -- Checks to see if the slot list includes the random weakness ID. If it does,\n -- removes it from the deck and replaces it with the ID of a random basic weakness provided by the\n -- all cards bag\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n --- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast\n --- if a weakness is added.\n internal.maybeDrawRandomWeakness = function(slots, playerColor)\n local randomWeaknessAmount = slots[RANDOM_WEAKNESS_ID] or 0\n slots[RANDOM_WEAKNESS_ID] = nil\n\n if randomWeaknessAmount \u003e 0 then\n for i = 1, randomWeaknessAmount do\n local weaknessId = allCardsBagApi.getRandomWeaknessId()\n slots[weaknessId] = (slots[weaknessId] or 0) + 1\n end\n internal.maybePrint(\"Added \" .. randomWeaknessAmount .. \" random basic weakness(es) to deck\", playerColor)\n end\n end\n\n -- Adds both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each\n ---@param deck table The processed ArkhamDB deck response\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the\n --- number of those cards which will be spawned\n ---@return string: Contains the name of the art that should be loaded (\"normal\", \"promo\" or \"revised\")\n internal.addInvestigatorCards = function(deck, slots)\n local investigatorId = deck.investigator_code\n slots[investigatorId .. \"-m\"] = 1\n local deckMeta = JSON.decode(deck.meta)\n\n -- handling alternative investigator art and parallel investigators\n local loadAltInvestigator = \"normal\"\n if deckMeta ~= nil then\n local altFrontId = tonumber(deckMeta.alternate_front) or 0\n local altBackId = tonumber(deckMeta.alternate_back) or 0\n local altArt = { front = \"normal\", back = \"normal\" }\n\n -- translating front ID\n if altFrontId \u003e 90000 and altFrontId \u003c 90100 then\n altArt.front = \"parallel\"\n elseif altFrontId \u003e 01500 and altFrontId \u003c 01506 then\n altArt.front = \"revised\"\n elseif altFrontId \u003e 98000 then\n altArt.front = \"promo\"\n end\n\n -- translating back ID\n if altBackId \u003e 90000 and altBackId \u003c 90100 then\n altArt.back = \"parallel\"\n elseif altBackId \u003e 01500 and altBackId \u003c 01506 then\n altArt.back = \"revised\"\n elseif altBackId \u003e 98000 then\n altArt.back = \"promo\"\n end\n\n -- updating investigatorID based on alt investigator selection\n -- precedence: parallel \u003e promo \u003e revised\n if altArt.front == \"parallel\" then\n if altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-p\"\n else\n investigatorId = investigatorId .. \"-pf\"\n end\n elseif altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-pb\"\n elseif altArt.front == \"promo\" or altArt.back == \"promo\" then\n loadAltInvestigator = \"promo\"\n elseif altArt.front == \"revised\" or altArt.back == \"revised\" then\n loadAltInvestigator = \"revised\"\n end\n end\n slots[investigatorId] = 1\n deck.investigator_code = investigatorId\n return loadAltInvestigator\n end\n\n -- Process the card list looking for the customizable cards, and add their upgrade sheets if needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddUpgradeSheets = function(slots)\n for cardId, _ in pairs(slots) do\n -- upgrade sheets for customizable cards\n local upgradesheet = allCardsBagApi.getCardById(cardId .. \"-c\")\n if upgradesheet ~= nil then\n slots[cardId .. \"-c\"] = 1\n end\n end\n end\n\n -- Process the card list looking for the Summoned Servitor, and add its minicard to the list if\n -- needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddSummonedServitor = function(slots)\n if slots[\"09080\"] ~= nil then\n slots[\"09080-m\"] = 1\n end\n end\n\n -- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1. Update\n -- the count based on the investigator count\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast if an error occurs\n internal.maybeAddOnTheMend = function(slots, playerColor)\n if slots[\"09006\"] ~= nil then\n local investigatorCount = playAreaApi.getInvestigatorCount()\n if investigatorCount ~= nil then\n slots[\"09006\"] = investigatorCount\n else\n internal.maybePrint(\"Something went wrong with the load, adding 4 copies of On the Mend\", playerColor)\n slots[\"09006\"] = 4\n end\n end\n end\n\n -- Process the card list looking for Reality Acid and adds the reference sheet when needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddRealityAcidReference = function(slots)\n if slots[\"89004\"] ~= nil then\n slots[\"89005\"] = 1\n end\n end\n\n -- Processes the deck description from ArkhamDB and modifies the slot list accordingly\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n ---@param description string The deck desription from ArkhamDB\n internal.maybeModifyDeckFromDescription = function(slots, description, playerColor)\n -- check for import instructions\n local pos = string.find(description, \"++SCED import instructions++\")\n if not pos then return end\n\n -- remove everything before instructions\n local tempStr = string.sub(description, pos)\n\n -- parse each line in instructions\n for line in tempStr:gmatch(\"([^\\n]+)\") do\n -- remove dashes at the start\n line = line:gsub(\"%- \", \"\")\n\n -- remove spaces\n line = line:gsub(\"%s\", \"\")\n\n -- remove balanced brackets\n line = line:gsub(\"%b()\", \"\")\n line = line:gsub(\"%b[]\", \"\")\n\n -- get instructor\n local instructor = \"\"\n for word in line:gmatch(\"%a+:\") do\n instructor = word\n break\n end\n\n -- go to the next line if no valid instructor found\n if instructor ~= \"add:\" and instructor ~= \"remove:\" then\n goto nextLine\n end\n\n -- remove instructor from line\n line = line:gsub(instructor, \"\")\n\n -- evaluate instructions\n for str in line:gmatch(\"([^,]+)\") do\n if instructor == \"add:\" then\n slots[str] = (slots[str] or 0) + 1\n elseif instructor == \"remove:\" then\n if slots[str] == nil then\n internal.maybePrint(\"Tried to remove card ID \" .. str .. \", but didn't find card in deck.\", playerColor)\n else\n slots[str] = math.max(slots[str] - 1, 0)\n\n -- fully remove cards that have a quantity of 0\n if slots[str] == 0 then\n slots[str] = nil\n\n -- also remove related minicard\n slots[str .. \"-m\"] = nil\n end\n end\n end\n end\n\n -- jump mark at the end of the loop\n ::nextLine::\n end\n end\n\n -- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.extractBondedCards = function(slots)\n -- Create a list of bonded cards first so we don't modify slots while iterating\n local bondedCards = { }\n local bondedList = { }\n for cardId, cardCount in pairs(slots) do\n local card = allCardsBagApi.getCardById(cardId)\n if card ~= nil and card.metadata.bonded ~= nil then\n for _, bond in ipairs(card.metadata.bonded) do\n -- 'unlimited' upper limit for cards without this data\n local maxCount = bond.maxCount or 99\n\n -- add a bonded card for each copy of the parent card (until the max value is reached)\n bondedCards[bond.id] = math.min(bond.count * cardCount, maxCount)\n\n -- We need to know which cards are bonded to determine their position, remember them\n bondedList[bond.id] = true\n\n -- Also adding taboo versions of bonded cards to the list\n bondedList[bond.id .. \"-t\"] = true\n end\n end\n end\n\n -- Add any bonded cards to the main slots list\n for bondedId, bondedCount in pairs(bondedCards) do\n slots[bondedId] = bondedCount\n end\n\n return bondedList\n end\n\n -- Check the deck for cards on its taboo list. If they're found, replace the entry in the slot with the Taboo id (i.e. \"XXXX\" becomes \"XXXX-t\")\n ---@param tabooId string The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.checkTaboos = function(tabooId, slots, playerColor)\n if tabooId then\n for cardId, _ in pairs(tabooList[tabooId].cards) do\n if slots[cardId] ~= nil then\n -- Make sure there's a taboo version of the card before we replace it\n -- SCED only maintains the most recent taboo cards. If a deck is using\n -- an older taboo list it's possible the card isn't a taboo any more\n local tabooCard = allCardsBagApi.getCardById(cardId .. \"-t\")\n if tabooCard == nil then\n local basicCard = allCardsBagApi.getCardById(cardId)\n internal.maybePrint(\"Taboo version for \" .. basicCard.data.Nickname .. \" is not available. Using standard version\", playerColor)\n else\n slots[cardId .. \"-t\"] = slots[cardId]\n slots[cardId] = nil\n end\n end\n end\n end\n end\n\n internal.maybePrint = function(message, playerColor)\n if playerColor ~= \"None\" then\n printToAll(message, playerColor)\n end\n end\n\n -- Gets the ArkhamDB config info from the configuration object.\n ---@return table: configuration data\n internal.getConfiguration = function()\n local configuration = getObjectsWithTag(\"import_configuration_provider\")[1].getTable(\"configuration\")\n printPriority = configuration.priority\n return configuration\n end\n\n internal.fixUtf16String = function(str)\n return str:gsub(\"\\\\u(%w%w%w%w)\", function(match)\n return string.char(tonumber(match, 16))\n end)\n end\n\n Request = {\n is_done = false,\n is_successful = false\n }\n\n -- Creates a new instance of a Request. Should not be directly called. Instead use Request.start() and Request.deferred().\n ---@param uri table\n ---@param configure fun(request, status)\n ---@return Request\n function Request:new(uri, configure)\n local this = {}\n\n setmetatable(this, self)\n self.__index = self\n\n if type(uri) == \"table\" then\n uri = table.concat(uri, \"/\")\n end\n\n this.uri = uri\n WebRequest.get(uri, function(status) configure(this, status) end)\n\n return this\n end\n\n -- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.\n -- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)\n ---@param uri table\n ---@param on_success fun(request, status, vararg)\n ---@param on_error fun(status)|nil\n ---@return Request\n function Request.deferred(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request:new(uri, function(request, status)\n if (status.is_done) then\n if (status.is_error) then\n request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error\n request.is_successful = false\n request.is_done = true\n else\n on_success(request, status)\n end\n end\n end)\n end\n\n -- Creates a new request. on_success should return whether the resultant data is as expected, and the processed content of the request.\n ---@param uri table\n ---@param on_success fun(status, vararg): boolean, any\n ---@param on_error nil|fun(status, vararg): string\n ---@return Request\n function Request.start(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request.deferred(uri, function(request, status)\n local result, message = on_success(status, table.unpack(parameters))\n if not result then request.error_message = message else request.content = message end\n request.is_successful = result\n request.is_done = true\n end, on_error, table.unpack(parameters))\n end\n\n ---@param requests Request[]\n ---@param on_success fun(content: any, vararg: any)\n ---@param on_error fun(requests: Request, vararg: any)|nil\n function Request.with_all(requests, on_success, on_error, ...)\n local parameters = table.pack(...)\n\n Wait.condition(function()\n local results = {}\n local errors = {}\n\n for _, request in ipairs(requests) do\n if request.is_successful then\n table.insert(results, request.content)\n else\n table.insert(errors, request)\n end\n end\n\n if (#errors \u003c= 0) then\n on_success(results, table.unpack(parameters))\n elseif on_error == nil then\n for _, request in ipairs(errors) do\n internal.maybePrint(table.concat({ \"[ERROR]\", request.uri, \":\", request.error_message }))\n end\n else\n on_error(requests, table.unpack(parameters))\n end\n end, function()\n for _, request in ipairs(requests) do\n if not request.is_done then return false end\n end\n return true\n end)\n end\n\n function Request:with(callback, ...)\n local arguments = table.pack(...)\n Wait.condition(function()\n if self.is_successful then\n callback(self.content, table.unpack(arguments))\n end\n end, function() return self.is_done\n end)\n end\n\n return ArkhamDb\nend\nend)\n__bundle_register(\"playercards/PlayerCardSpawner\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Amount to shift for the next card (zShift) or next row of cards (xShift)\n-- Note that the table rotation is weird, and the X axis is vertical while the\n-- Z axis is horizontal\nlocal SPREAD_Z_SHIFT = -2.3\nlocal SPREAD_X_SHIFT = -3.66\n\nSpawner = { }\n\n-- Spawns a list of cards at the given position/rotation. This will separate cards by size -\n-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If\n-- there are different types, the provided callback will be called once for each type as it spawns\n-- either a card or deck.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos tts__Vector table where the cards should be spawned (global)\n---@param rot tts__Vector table for the orientation of the spawned cards (global)\n---@param sort boolean True if this list of cards should be sorted before spawning\n---@param callback? function Callback to be called after the card/deck spawns.\nSpawner.spawnCards = function(cardList, pos, rot, sort, callback)\n if sort then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local miniCards = { }\n local standardCards = { }\n local investigatorCards = { }\n\n for _, card in ipairs(cardList) do\n if card.metadata.type == \"Investigator\" then\n table.insert(investigatorCards, card)\n elseif card.metadata.type == \"Minicard\" then\n -- set proper scale for minicards\n card.data.Transform.scaleX = 0.6\n card.data.Transform.scaleZ = 0.6\n table.insert(miniCards, card)\n else\n table.insert(standardCards, card)\n end\n end\n\n -- Spawn each of the three types individually. Y position accounts for the thickness of the spawned deck\n local position = { x = pos.x, y = pos.y, z = pos.z }\n Spawner.spawn(investigatorCards, position, rot, callback)\n\n position.y = position.y + (#investigatorCards + #standardCards) * 0.07\n Spawner.spawn(standardCards, position, rot, callback)\n\n position.y = position.y + (#standardCards + #miniCards) * 0.07\n Spawner.spawn(miniCards, position, rot, callback)\nend\n\nSpawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)\n if sort then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local position = { x = startPos.x, y = startPos.y, z = startPos.z }\n -- Special handle the first row if we have less than a full single row, but only if there's a\n -- reasonable max column count. Single-row spreads will send a large value for maxCols\n if maxCols \u003c 100 and #cardList \u003c maxCols then\n position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)\n end\n local cardsInRow = 0\n local rows = 0\n for _, card in ipairs(cardList) do\n Spawner.spawn({ card }, position, rot, callback)\n position.z = position.z + SPREAD_Z_SHIFT\n cardsInRow = cardsInRow + 1\n if cardsInRow \u003e= maxCols then\n rows = rows + 1\n local cardsForRow = #cardList - rows * maxCols\n if cardsForRow \u003e maxCols then\n cardsForRow = maxCols\n end\n position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)\n position.x = position.x + SPREAD_X_SHIFT\n cardsInRow = 0\n end\n end\nend\n\n-- Spawn a specific list of cards. This method is for internal use and should not be called\n-- directly, use spawnCards instead.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos table Position where the cards should be spawned (global)\n---@param rot table Rotation for the orientation of the spawned cards (global)\n---@param callback? function callback to be called after the card/deck spawns.\nSpawner.spawn = function(cardList, pos, rot, callback)\n if #cardList == 0 then return end\n\n -- Spawn a single card directly\n if #cardList == 1 then\n -- handle sideways card\n if cardList[1].data.SidewaysCard then\n rot = { rot.x, rot.y - 90, rot.z }\n end\n return spawnObjectData({\n data = cardList[1].data,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- For multiple cards, construct a deck and spawn that\n local deckScaleX = cardList[1].data.Transform.scaleX\n local deckScaleZ = cardList[1].data.Transform.scaleZ\n local deck = Spawner.buildDeckDataTemplate(deckScaleX, deckScaleZ)\n\n local sidewaysDeck = true\n for _, spawnCard in ipairs(cardList) do\n Spawner.addCardToDeck(deck, spawnCard.data)\n -- set sidewaysDeck to false if any card is not a sideways card\n sidewaysDeck = (sidewaysDeck and spawnCard.data.SidewaysCard)\n end\n\n -- set the alt view angle for sideways decks\n if sidewaysDeck then\n deck.AltLookAngle = { x = 0, y = 180, z = 90 }\n rot = { rot.x, rot.y - 90, rot.z }\n end\n\n return spawnObjectData({\n data = deck,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\nend\n\n-- Inserts a card into the given deck. This does three things:\n-- 1. Add the card's data to ContainedObjects\n-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's\n-- ID list. Note that the deck's ID list is \"DeckIDs\" even though it\n-- contains a list of card Ids\n-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's\n-- \"CustomDeck\" field is a list of all CustomDecks used by cards within the\n-- deck, keyed by the DeckID and referencing the custom deck table\n---@param deck table TTS deck data structure to add to\n---@param cardData table Data for the card to be inserted\nSpawner.addCardToDeck = function(deck, cardData)\n for customDeckId, customDeckData in pairs(cardData.CustomDeck) do\n if (deck.CustomDeck[customDeckId] == nil) then\n -- CustomDeck not added to deck yet, add it\n deck.CustomDeck[customDeckId] = customDeckData\n elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then\n -- CustomDeck for this card matches the current one for the deck, do nothing\n else\n -- CustomDeck data conflict\n local newDeckId = nil\n for deckId, customDeck in pairs(deck.CustomDeck) do\n if (customDeckData.FaceURL == customDeck.FaceURL) then\n newDeckId = deckId\n end\n end\n if (newDeckId == nil) then\n -- No non-conflicting custom deck for this card, add a new one\n newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, \"1000\")\n deck.CustomDeck[newDeckId] = customDeckData\n end\n -- Update the card with the new CustomDeck info\n cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)\n cardData.CustomDeck[customDeckId] = nil\n cardData.CustomDeck[newDeckId] = customDeckData\n break\n end\n end\n table.insert(deck.ContainedObjects, cardData)\n table.insert(deck.DeckIDs, cardData.CardID)\nend\n\n-- Create an empty deck data table which can have cards added to it. This\n-- creates a new table on each call without using metatables or previous\n-- definitions because we can't be sure that TTS doesn't modify the structure\n---@return table deck Table containing the minimal TTS deck data structure\nSpawner.buildDeckDataTemplate = function(deckScaleX, deckScaleZ)\n local deck = {}\n deck.Name = \"Deck\"\n\n -- Card data. DeckIDs and CustomDeck entries will be built from the cards\n deck.ContainedObjects = {}\n deck.DeckIDs = {}\n deck.CustomDeck = {}\n\n -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here\n -- Decks won't inherently scale to the cards in them. The card list being spawned should be all\n -- the same type/size by this point, so use the first card to set the size\n deck.Transform = {\n scaleX = deckScaleX or 1,\n scaleY = 1,\n scaleZ = deckScaleZ or 1,\n }\n\n return deck\nend\n\n-- Returns the first ID which does not exist in the given table, starting at startId and increasing\n---@param objectTable table keyed by strings which are numbers\n---@param startId string possible ID.\n---@return string id \u003e= startId\nSpawner.findNextAvailableId = function(objectTable, startId)\n local id = startId\n while objectTable[id] ~= nil do\n id = tostring(tonumber(id) + 1)\n end\n return id\nend\n\n-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.\n---@return number PBCN 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are\n-- irrelevant as they provide only grouping and the order between them doesn't matter.\nSpawner.getpbcn = function(metadata)\n if metadata.permanent then\n return 1\n elseif metadata.bonded_to ~= nil then\n return 2\n else -- Normal card\n return 3\n end\nend\n\n-- Comparison function used to sort the cards in a deck. Groups bonded or\n-- permanent cards first, then sorts within theose types by name/subname.\n-- Normal cards will sort in standard alphabetical order, while\n-- permanent/bonded/customizable will be in reverse alphabetical order.\n--\n-- Since cards spawn in the order provided by this comparator, with the first\n-- cards ending up at the bottom of a pile, this ordering will spawn in reverse\n-- alphabetical order. This presents the cards in order for non-face-down\n-- areas, and presents them in order when Searching the face-down deck.\nSpawner.cardComparator = function(card1, card2)\n local pbcn1 = Spawner.getpbcn(card1.metadata)\n local pbcn2 = Spawner.getpbcn(card2.metadata)\n if pbcn1 ~= pbcn2 then\n return pbcn1 \u003e pbcn2\n end\n if pbcn1 == 3 then\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n end\n return card1.data.Description \u003c card2.data.Description\n else\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003e card2.data.Nickname\n end\n return card1.data.Description \u003e card2.data.Description\n end\nend\nend)\n__bundle_register(\"playermat/Zones\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Sets up and returns coordinates for all possible spawn zones. Because Lua assigns tables by reference\n-- and there is no built-in function to copy a table this is relatively brute force.\n--\n-- Positions are all relative to the player mat, and most are consistent. The\n-- exception are the SetAside# zones, which are placed to the left of the mat\n-- for White/Green, and the right of the mat for Orange/Red.\n--\n-- Investigator: Investigator card area.\n-- Minicard: Placement for the investigator's minicard, just above the player mat\n-- Deck, Discard: Standard locations for the deck and discard piles.\n-- Blank1: used for assets that start in play (e.g. Duke)\n-- Tarot, Hand1, Hand2, Ally, Blank4, Accessory, Arcane1, Arcane2, Body: Asset slot positions\n-- Threat[1-4]: Threat area slots. Threat[1-3] correspond to the named threat area slots, and Threat4 is the blank threat area slot.\n-- SetAside[1-3]: Column closest to the player mat, with 1 at the top and 3 at the bottom.\n-- SetAside[4-6]: Column farther away from the mat, with 4 at the top and 6 at the bottom.\n-- SetAside1: Permanent cards\n-- SetAside2: Bonded cards\n-- SetAside3: Ancestral Knowledge / Underworld Market\n-- SetAside4: Upgrade sheets for customizable cards\n-- SetAside5: Hunch Deck for Joe Diamond\n-- SetAside6: currently unused\ndo\n local playermatApi = require(\"playermat/PlayermatApi\")\n local Zones = { }\n\n local commonZones = {}\n commonZones[\"Investigator\"] = { -1.177, 0, 0.002 }\n commonZones[\"Deck\"] = { -1.82, 0, 0 }\n commonZones[\"Discard\"] = { -1.82, 0, 0.61 }\n commonZones[\"Ally\"] = { -0.615, 0, 0.024 }\n commonZones[\"Body\"] = { -0.630, 0, 0.553 }\n commonZones[\"Hand1\"] = { 0.215, 0, 0.042 }\n commonZones[\"Hand2\"] = { -0.180, 0, 0.037 }\n commonZones[\"Arcane1\"] = { 0.212, 0, 0.559 }\n commonZones[\"Arcane2\"] = { -0.171, 0, 0.557 }\n commonZones[\"Tarot\"] = { 0.602, 0, 0.033 }\n commonZones[\"Accessory\"] = { 0.602, 0, 0.555 }\n commonZones[\"Blank1\"] = { 1.758, 0, 0.040 }\n commonZones[\"Blank2\"] = { 1.754, 0, 0.563 }\n commonZones[\"Blank3\"] = { 1.371, 0, 0.038 }\n commonZones[\"Blank4\"] = { 1.371, 0, 0.558 }\n commonZones[\"Blank5\"] = { 0.98, 0, 0.035 }\n commonZones[\"Blank6\"] = { 0.977, 0, 0.556 }\n commonZones[\"Threat1\"] = { -0.911, 0, -0.625 }\n commonZones[\"Threat2\"] = { -0.454, 0, -0.625 }\n commonZones[\"Threat3\"] = { 0.002, 0, -0.625 }\n commonZones[\"Threat4\"] = { 0.459, 0, -0.625 }\n\n local zoneData = {}\n zoneData[\"White\"] = {}\n zoneData[\"White\"][\"Investigator\"] = commonZones[\"Investigator\"]\n zoneData[\"White\"][\"Deck\"] = commonZones[\"Deck\"]\n zoneData[\"White\"][\"Discard\"] = commonZones[\"Discard\"]\n zoneData[\"White\"][\"Ally\"] = commonZones[\"Ally\"]\n zoneData[\"White\"][\"Body\"] = commonZones[\"Body\"]\n zoneData[\"White\"][\"Hand1\"] = commonZones[\"Hand1\"]\n zoneData[\"White\"][\"Hand2\"] = commonZones[\"Hand2\"]\n zoneData[\"White\"][\"Arcane1\"] = commonZones[\"Arcane1\"]\n zoneData[\"White\"][\"Arcane2\"] = commonZones[\"Arcane2\"]\n zoneData[\"White\"][\"Tarot\"] = commonZones[\"Tarot\"]\n zoneData[\"White\"][\"Accessory\"] = commonZones[\"Accessory\"]\n zoneData[\"White\"][\"Blank1\"] = commonZones[\"Blank1\"]\n zoneData[\"White\"][\"Blank2\"] = commonZones[\"Blank2\"]\n zoneData[\"White\"][\"Blank3\"] = commonZones[\"Blank3\"]\n zoneData[\"White\"][\"Blank4\"] = commonZones[\"Blank4\"]\n zoneData[\"White\"][\"Blank5\"] = commonZones[\"Blank5\"]\n zoneData[\"White\"][\"Blank6\"] = commonZones[\"Blank6\"]\n zoneData[\"White\"][\"Threat1\"] = commonZones[\"Threat1\"]\n zoneData[\"White\"][\"Threat2\"] = commonZones[\"Threat2\"]\n zoneData[\"White\"][\"Threat3\"] = commonZones[\"Threat3\"]\n zoneData[\"White\"][\"Threat4\"] = commonZones[\"Threat4\"]\n zoneData[\"White\"][\"Minicard\"] = { -1, 0, -1.45 }\n zoneData[\"White\"][\"SetAside1\"] = { 2.35, 0, -0.520 }\n zoneData[\"White\"][\"SetAside2\"] = { 2.35, 0, 0.042 }\n zoneData[\"White\"][\"SetAside3\"] = { 2.35, 0, 0.605 }\n zoneData[\"White\"][\"UnderSetAside3\"] = { 2.50, 0, 0.805 }\n zoneData[\"White\"][\"SetAside4\"] = { 2.78, 0, -0.520 }\n zoneData[\"White\"][\"SetAside5\"] = { 2.78, 0, 0.042 }\n zoneData[\"White\"][\"SetAside6\"] = { 2.78, 0, 0.605 }\n zoneData[\"White\"][\"UnderSetAside6\"] = { 2.93, 0, 0.805 }\n\n zoneData[\"Orange\"] = {}\n zoneData[\"Orange\"][\"Investigator\"] = commonZones[\"Investigator\"]\n zoneData[\"Orange\"][\"Deck\"] = commonZones[\"Deck\"]\n zoneData[\"Orange\"][\"Discard\"] = commonZones[\"Discard\"]\n zoneData[\"Orange\"][\"Ally\"] = commonZones[\"Ally\"]\n zoneData[\"Orange\"][\"Body\"] = commonZones[\"Body\"]\n zoneData[\"Orange\"][\"Hand1\"] = commonZones[\"Hand1\"]\n zoneData[\"Orange\"][\"Hand2\"] = commonZones[\"Hand2\"]\n zoneData[\"Orange\"][\"Arcane1\"] = commonZones[\"Arcane1\"]\n zoneData[\"Orange\"][\"Arcane2\"] = commonZones[\"Arcane2\"]\n zoneData[\"Orange\"][\"Tarot\"] = commonZones[\"Tarot\"]\n zoneData[\"Orange\"][\"Accessory\"] = commonZones[\"Accessory\"]\n zoneData[\"Orange\"][\"Blank1\"] = commonZones[\"Blank1\"]\n zoneData[\"Orange\"][\"Blank2\"] = commonZones[\"Blank2\"]\n zoneData[\"Orange\"][\"Blank3\"] = commonZones[\"Blank3\"]\n zoneData[\"Orange\"][\"Blank4\"] = commonZones[\"Blank4\"]\n zoneData[\"Orange\"][\"Blank5\"] = commonZones[\"Blank5\"]\n zoneData[\"Orange\"][\"Blank6\"] = commonZones[\"Blank6\"]\n zoneData[\"Orange\"][\"Threat1\"] = commonZones[\"Threat1\"]\n zoneData[\"Orange\"][\"Threat2\"] = commonZones[\"Threat2\"]\n zoneData[\"Orange\"][\"Threat3\"] = commonZones[\"Threat3\"]\n zoneData[\"Orange\"][\"Threat4\"] = commonZones[\"Threat4\"]\n zoneData[\"Orange\"][\"Minicard\"] = { 1, 0, -1.45 }\n zoneData[\"Orange\"][\"SetAside1\"] = { -2.35, 0, -0.520 }\n zoneData[\"Orange\"][\"SetAside2\"] = { -2.35, 0, 0.042}\n zoneData[\"Orange\"][\"SetAside3\"] = { -2.35, 0, 0.605 }\n zoneData[\"Orange\"][\"UnderSetAside3\"] = { -2.50, 0, 0.805 }\n zoneData[\"Orange\"][\"SetAside4\"] = { -2.78, 0, -0.520 }\n zoneData[\"Orange\"][\"SetAside5\"] = { -2.78, 0, 0.042 }\n zoneData[\"Orange\"][\"SetAside6\"] = { -2.78, 0, 0.605 }\n zoneData[\"Orange\"][\"UnderSetAside6\"] = { -2.93, 0, 0.805 }\n\n -- Green positions are the same as White and Red the same as Orange\n zoneData[\"Red\"] = zoneData[\"Orange\"]\n zoneData[\"Green\"] = zoneData[\"White\"]\n\n -- Gets the global position for the given zone on the specified player mat.\n ---@param playerColor string Color name of the player mat to get the zone position for (e.g. \"Red\")\n ---@param zoneName string Name of the zone to get the position for. See Zones object documentation for a list of valid zones.\n ---@return tts__Vector|nil: Global position table, or nil if an invalid player color or zone is specified\n Zones.getZonePosition = function(playerColor, zoneName)\n if (playerColor ~= \"Red\"\n and playerColor ~= \"Orange\"\n and playerColor ~= \"White\"\n and playerColor ~= \"Green\") then\n return nil\n end\n return playermatApi.transformLocalPosition(zoneData[playerColor][zoneName], playerColor)\n end\n\n -- Return the global rotation for a card on the given player mat, based on its zone.\n ---@param playerColor string Color name of the player mat to get the rotation for (e.g. \"Red\")\n ---@param zoneName string Name of the zone. See Zones object documentation for a list of valid zones.\n ---@return tts__Vector: Global rotation vector for the given card. This will include the\n -- Y rotation to orient the card on the given player mat as well as a\n -- Z rotation to place the card face up or face down.\n Zones.getDefaultCardRotation = function(playerColor, zoneName)\n local cardRotation = playermatApi.returnRotation(playerColor)\n if zoneName == \"Deck\" then\n cardRotation = cardRotation + Vector(0, 0, 180)\n end\n return cardRotation\n end\n\n return Zones\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"greenDeck\":\"\",\"investigators\":true,\"loadNewest\":true,\"orangeDeck\":\"\",\"privateDeck\":true,\"redDeck\":\"\",\"whiteDeck\":\"\"}", "MeasureMovement": false, "Name": "Custom_Tile", @@ -88538,8 +32256,8 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"util/ConnectionDrawingTool\")\nend)\n__bundle_register(\"util/ConnectionDrawingTool\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal lines = {}\n\n-- save \"lines\" to be able to remove them after loading\nfunction onSave()\n return JSON.encode(lines)\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n lines = JSON.decode(savedData) or {}\n end\nend\n\n-- create timer when numpad 0 is pressed\nfunction onScriptingButtonDown(index, player_color)\n if index ~= 10 then return end\n TimerID = Wait.time(function() draw_from(Player[player_color]) end, 1)\nend\n\n-- called for long press of numpad 0, draws lines from hovered object to selected objects\nfunction draw_from(player)\n local source = player.getHoverObject()\n if not source then return end\n\n for _, item in ipairs(player.getSelectedObjects()) do\n if item.getGUID() ~= source.getGUID() then\n if item.getGUID() \u003e source.getGUID() then\n draw_with_pair(item, source)\n else\n draw_with_pair(source, item)\n end\n end\n end\n\n process_lines()\nend\n\n-- general drawing of all lines between selected objects\nfunction onScriptingButtonUp(index, player_color)\n if index ~= 10 then return end\n -- returns true only if there is a timer to cancel. If this is false then we've waited longer than a second.\n if not Wait.stop(TimerID) then return end\n\n local items = Player[player_color].getSelectedObjects()\n if #items \u003c 2 then\n broadcastToColor(\"You must have at least two items selected (currently: \" .. #items .. \").\", player_color, \"Red\")\n return\n end\n\n table.sort(items, function(a, b) return a.getGUID() \u003e b.getGUID() end)\n\n for f = 1, #items - 1 do\n for s = f + 1, #items do\n draw_with_pair(items[f], items[s])\n end\n end\n\n process_lines()\nend\n\n-- adds two objects to table of vector lines\nfunction draw_with_pair(first, second)\n local guid_first = first.getGUID()\n local guid_second = second.getGUID()\n\n if Global.getVectorLines() == nil then lines = {} end\n if not lines[guid_first] then lines[guid_first] = {} end\n\n if lines[guid_first][guid_second] then\n lines[guid_first][guid_second] = nil\n else\n lines[guid_first][guid_second] = { points = { first.getPosition(), second.getPosition() }, color = \"White\" }\n end\nend\n\n-- updates the global vector lines based on \"lines\"\nfunction process_lines()\n local drawing = {}\n\n for _, first in pairs(lines) do\n for _, data in pairs(first) do\n table.insert(drawing, data)\n end\n end\n\n Global.setVectorLines(drawing)\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "{\"e8e04b\":[]}", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"util/ConnectionDrawingTool\")\nend)\n__bundle_register(\"util/ConnectionDrawingTool\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal connections = {}\n\nfunction onSave()\n return JSON.encode({ connections = connections })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData) or {}\n connections = loadedData.connections\n processLines()\n end\n\n addHotkey(\"Drawing Tool: Reset\", function() connections = {} processLines() end)\n addHotkey(\"Drawing Tool: Redraw\", processLines)\nend\n\nfunction onScriptingButtonDown(index, playerColor)\n if index ~= 10 then return end\n\n Timer.create {\n identifier = playerColor .. \"_draw_from\",\n function_name = \"draw_from\",\n parameters = { player = Player[playerColor] },\n delay = 1\n }\nend\n\nfunction draw_from(params)\n local source = params.player.getHoverObject()\n if not source then return end\n\n for _, item in ipairs(params.player.getSelectedObjects()) do\n if item ~= source then\n if item.getGUID() \u003e source.getGUID() then\n addPair(item, source)\n else\n addPair(source, item)\n end\n end\n end\n\n processLines()\nend\n\nfunction onScriptingButtonUp(index, playerColor)\n if index ~= 10 then return end\n\n -- returns true only if there is a timer to cancel. If this is false then we've waited longer than a second.\n if not Timer.destroy(playerColor .. \"_draw_from\") then return end\n\n local items = Player[playerColor].getSelectedObjects()\n if #items \u003c 2 then return end\n\n table.sort(items, function(a, b) return a.getGUID() \u003e b.getGUID() end)\n\n for i = 1, #items do\n local first = items[i]\n\n for j = i, #items do\n local second = items[j]\n addPair(first, second)\n end\n end\n\n processLines()\nend\n\nfunction addPair(first, second)\n local first_guid = first.getGUID()\n local second_guid = second.getGUID()\n\n if not connections[first_guid] then connections[first_guid] = {} end\n connections[first_guid][second_guid] = not connections[first_guid][second_guid]\nend\n\nfunction processLines()\n local lines = {}\n\n for source_guid, target_guids in pairs(connections) do\n local source = getObjectFromGUID(source_guid)\n\n for target_guid, exists in pairs(target_guids) do\n if exists then\n local target = getObjectFromGUID(target_guid)\n\n if source and target then\n table.insert(lines, {\n points = { source.getPosition(), target.getPosition() },\n color = Color.White\n })\n end\n end\n end\n end\n\n Global.setVectorLines(lines)\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"connections\":[]}", "MeasureMovement": false, "Name": "Custom_Token", "Nickname": "Drawing Tool", @@ -88584,7 +32302,7 @@ "ImageURL": "https://i.imgur.com/gs1mtXJ.png", "WidthScale": 0 }, - "Description": "Allows changing of the playmat image. Provide URL to the image or leave empty for default image.", + "Description": "Allows changing of the playarea image. Provide URL to the image or leave empty for default image.", "DragSelectable": true, "GMNotes": "", "GUID": "b7b45b", @@ -88595,11 +32313,11 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PlayAreaSelector\")\nend)\n__bundle_register(\"core/PlayAreaSelector\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PlayAreaImageData\") -- this fills the variable \"PLAYAREA_IMAGE_DATA\"\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal typeIndex, selectionIndex, plainNameCache\n\nfunction onSave() return JSON.encode({ typeIndex = typeIndex, selectionIndex = selectionIndex }) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData) or {}\n typeIndex = loadedData.typeIndex or 1\n selectionIndex = loadedData.selectionIndex or 1\n end\n\n self.createButton({\n function_owner = self,\n click_function = \"onClick_toggleGallery\",\n tooltip = \"Show Image Gallery\",\n position = {0, 0.06, 0},\n height = 1500,\n width = 1500,\n color = { 1, 1, 1, 0 }\n })\n\n Wait.time(updatePlayAreaGallery, 0.5)\n math.randomseed(os.time())\nend\n\n-- click function for main button\nfunction onClick_toggleGallery(_, playerColor)\n Global.call(\"togglePlayAreaGallery\", playerColor)\nend\n\nfunction getDataSubTableByIndex(dataTable, index)\n local loopId = 1\n for i, v in pairs(dataTable) do\n if index == loopId then return v end\n loopId = loopId + 1\n end\n return {}\nend\n\nfunction updatePlayAreaGallery()\n -- get subtables\n local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex)\n local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex)\n\n -- get global xml to insert elements\n local globalXml = UI.getXmlTable()\n\n -- selectable items\n local itemSelection = getXmlTableElementById(globalXml, 'itemSelection')\n itemSelection.children = {}\n\n local i = 0\n for itemName, _ in pairs(dataForType) do\n i = i + 1\n table.insert(itemSelection.children,\n {\n tag = \"Panel\",\n attributes = { class = \"itemPanel\", id = \"typePanel\" .. i },\n children = {\n tag = \"Text\",\n value = itemName,\n attributes = { class = \"itemText\", id = \"typeListText\" .. i }\n }\n })\n end\n\n -- selectable images for that item\n local playareaList = getXmlTableElementById(globalXml, 'playareaList')\n playareaList.children = {}\n\n for i, v in ipairs(dataForSelection) do\n table.insert(playareaList.children,\n {\n tag = \"VerticalLayout\",\n attributes = { class = \"imageBox\", id = \"image\" .. i },\n children = {\n {\n tag = 'Image',\n attributes = { class = \"playareaImage\", image = v.URL }\n },\n {\n tag = 'Text',\n value = v.Name,\n attributes = { class = \"imageName\" }\n }\n }\n })\n end\n\n playareaList.attributes.height = round(#playareaList.children / 2, 0) * 380\n Global.call(\"updateGlobalXml\", globalXml)\n Wait.time(highlightTabAndItem, 0.1)\nend\n\nfunction onClick_imageTab(_, _, tabId)\n typeIndex = tonumber(tabId:sub(9))\n selectionIndex = 1\n updatePlayAreaGallery()\nend\n\nfunction onClick_listItem(_, _, listId)\n selectionIndex = tonumber(listId:sub(10))\n updatePlayAreaGallery()\nend\n\nfunction onClick_image(player, _, id)\n local imageIndex = tonumber(id:sub(6))\n local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex)\n local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex)\n local newURL = dataForSelection[imageIndex].URL\n playAreaApi.updateSurface(newURL)\n Global.call(\"togglePlayAreaGallery\", player.color)\nend\n\nfunction highlightTabAndItem()\n -- highlight active tab\n for i = 1, 5 do\n local color = \"#888888\"\n if i == typeIndex then color = \"#ffffff\" end\n UI.setAttribute(\"imageTab\" .. i, \"color\", color)\n end\n\n -- highlight item\n UI.setAttribute(\"typePanel\" .. selectionIndex, \"color\", \"grey\")\n UI.setAttribute(\"typeListText\" .. selectionIndex, \"color\", \"black\")\nend\n\n-- loops through an XML table and returns the specified object\n---@param ui table XmlTable (get this via getXmlTable)\n---@param id string Id of the object to return\nfunction getXmlTableElementById(ui, id)\n for _, obj in ipairs(ui) do\n if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end\n if obj.children then\n local result = getXmlTableElementById(obj.children, id)\n if result then return result end\n end\n end\n return nil\nend\n\n-- utility function\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\nfunction maybeUpdatePlayAreaImage(scenarioName)\n -- check if option is enabled\n local optionPanelState = optionPanelApi.getOptions()\n if not optionPanelState[\"changePlayAreaImage\"] then return end\n\n -- initialize cache if nil\n if not plainNameCache then\n plainNameCache = {}\n for i, dataForType in pairs(PLAYAREA_IMAGE_DATA) do\n for j, dataForCycle in pairs(dataForType) do\n for k, data in ipairs(dataForCycle) do\n local plainName = getPlainName(data.Name)\n \n -- override plainName for all images in the \"Other Images\" category (except the default image)\n if i == \"Other Images\" and data.Name ~= \"Default Image\" then\n plainName = \"Generic\"\n end\n\n if not plainNameCache[plainName] then\n plainNameCache[plainName] = {}\n end\n table.insert(plainNameCache[plainName], data.URL)\n end\n end\n end\n end\n\n -- look for matching playarea image or use generic ones instead\n local listOfEligibleImages = {}\n if plainNameCache[scenarioName] then\n listOfEligibleImages = plainNameCache[scenarioName]\n else\n listOfEligibleImages = plainNameCache[\"Generic\"]\n end\n\n -- get a random entry from the eligible list\n local newImageIndex = math.random(#listOfEligibleImages)\n playAreaApi.updateSurface(listOfEligibleImages[newImageIndex])\nend\n\n-- attempts to extract the plain scenario name from the playarea image name\nfunction getPlainName(str)\n -- remove prefix type 1\n str = str:gsub(\"%w+%-%w%s%-%s\", \"\") -- matches \"II-B - Thousand Shapes of Horror 1\"\n \n -- remove prefix type 2\n str = str:gsub(\"%w+%-%w%s\", \"\") -- matches \"59-Z Congress of Keys 1\"\n\n -- remove prefix type 3\n str = str:gsub(\"%w+%s%-%s\", \"\") -- matches \"III - The Secret Name 4\"\n\n -- remove prefix type 4\n str = str:gsub(\"%?+%s%-%s\", \"\") -- matches \"??? - Fatal Mirage\"\n\n -- remove suffix (numbering)\n str = str:gsub(\"%s%d+\", \"\")\n\n return str\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/PlayAreaImageData\", function(require, _LOADED, __bundle_register, __bundle_modules)\nPLAYAREA_IMAGE_DATA = {\n [\"Official Campaigns\"] = {\n [\"Night of the Zealot\"] = {\n {\n Name = \"I - The Gathering 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725443007/D34B55D2637EF1DF22839D12F9CF74F92F8EB486/\"\n },\n {\n Name = \"III - The Devourer Below 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725443203/FBE04C8B89F79D18C6D29C28DC3B292A5A3A3DEB/\"\n },\n {\n Name = \"III - The Devourer Below 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725443345/FD10BC04B3F3AFD710C3C12EE14F85F9AFB265E6/\"\n }\n },\n [\"The Dunwich Legacy\"] = {\n {\n Name = \"I-A - Extracurricular Activity 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725401398/AC55F0754B7E4A39796A6F2236012AE03DA55E20/\"\n },\n {\n Name = \"I-A - Extracurricular Activity 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725401658/9F3ED6E256818C4528D55980B9D7E44B87170A4F/\"\n },\n {\n Name = \"I-A - Extracurricular Activity 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725401870/FE29404D9D93BE2735D41C115DDD9708A0931F3E/\"\n },\n {\n Name = \"I-B - The House Always Wins 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402059/4C4A50D259ECFD4DFB94FABB9FA2AD3AABDCA0CA/\"\n },\n {\n Name = \"I-B - The House Always Wins 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402263/8B0C981B78E803B3BFD64AC874F315B6810E579B/\"\n },\n {\n Name = \"I-B - The House Always Wins 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402460/8599D17306C644D75D189591BC6D57C2F27F9E35/\"\n },\n {\n Name = \"I-B - The House Always Wins 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402641/777865B25BCF86C2EFEAE232AFBC31561D12AE0F/\"\n },\n {\n Name = \"II - The Miskatonic Museum 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402804/A44E68AC08E6568A757429B29E71C703B76FA159/\"\n },\n {\n Name = \"II - The Miskatonic Museum 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403000/B28D46D5B7BCE9EA9BF27D3A0A932393D8258A76/\"\n },\n {\n Name = \"III - The Essex County Express\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403150/956BE4B5C25805E57C8FC5CF28A94BD7EF07CA54/\"\n },\n {\n Name = \"IV - Blood on the Altar 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403290/07DD1508BA880AD1212A820BC29471C48DC90C53/\"\n },\n {\n Name = \"IV - Blood on the Altar 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403456/A74398E8CCD78657C2555832A3B340723E8C9117/\"\n },\n {\n Name = \"IV - Blood on the Altar 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403636/CD89D7D9CC020F41D5AD02D54E308877583EC45F/\"\n },\n {\n Name = \"IV - Blood on the Altar 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403818/AEFAC50DAC82271BCACC53B8FF69B5B094FA1078/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403955/E7C566E536CF6E910DB654FC1DBF4838F2BAF899/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404127/F92F123BBD3D857C579B9284988C3AFFBCD84B2B/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404273/7F8CFE6785BD74BB8D5E34C204AFB8AA580CA3E3/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404424/C2AE08371CF2DE2777791BBD4AEF446E50532382/\"\n },\n {\n Name = \"VI - Where Doom Awaits 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404578/A520CE0F6B0C8591A48ADEF34E68B46AFC2BC83B/\"\n },\n {\n Name = \"VI - Where Doom Awaits 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404740/1FF9EF375BDBB73CA8F0A1562F215692AAEA7131/\"\n },\n {\n Name = \"VI - Where Doom Awaits 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404914/5473D20CE0122F55EEADB16C4353D4EDD91E440E/\"\n },\n {\n Name = \"VI - Where Doom Awaits 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725405088/A0AA8CB864747152763D434D0737D81EB515E71B/\"\n },\n {\n Name = \"VI - Where Doom Awaits 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725405247/FAF408DAAD4FC72142DFDFE5FCEA4B84293AA72C/\"\n },\n {\n Name = \"VII - Lost in Time and Space 1\",\n URL = \"https://i.ibb.co/rtTpbDx/Dunwich-8-Lost-in-Time-amp-Space.jpg\"\n },\n {\n Name = \"VII - Lost in Time and Space 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725405746/771BAE40F98BB16F8D011FA794E4AC0095131AF1/\"\n },\n {\n Name = \"VII - Lost in Time and Space 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725406148/D15D56EA34F27C651D7E7AC202DA4DEBE395E310/\"\n }\n },\n [\"The Path to Carcosa\"] = {\n {\n Name = \"I - Curtain Call\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426327/41F6192EDCFFD6AAE2EE44C2BB5708B19D7464A9/\"\n },\n {\n Name = \"II - The Last King 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426499/114EAEA245AC51CA219364AF26341E7F7E649A7D/\"\n },\n {\n Name = \"II - The Last King 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426683/A2762E95DD79D7A7BC749925166BBFC18B62EF3B/\"\n },\n {\n Name = \"II - The Last King 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426818/A6A20773EA95CE3D9896B22E317A0E1ACCA911F0/\"\n },\n {\n Name = \"III - Echoes of the Past\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427005/25F51DE4B1F33C16A0E68C929E5002F0C4520A37/\"\n },\n {\n Name = \"IV - The Unspeakable Oath 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427178/9575E4F6E53DDAD2D3E61684AB9757B04E1EF787/\"\n },\n {\n Name = \"IV - The Unspeakable Oath 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427342/199A6C6411992ACFB8A417E86207FE6CE956EB97/\"\n },\n {\n Name = \"IV - The Unspeakable Oath 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427501/48525BCDFA281947FA9ED26507BC1F81C07056E8/\"\n },\n {\n Name = \"V - A Phantom of Truth 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427681/F72B02FF1A5E58CBCB0E53C8455310AF37064477/\"\n },\n {\n Name = \"V - A Phantom of Truth 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427848/215A757FF28317EB3EF7CF1F9CAE6F1CFF735091/\"\n },\n {\n Name = \"VI - The Pallid Mask 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428067/D46BAD80B112156A2D3DEFF247A74C74F27DDA12/\"\n },\n {\n Name = \"VI - The Pallid Mask 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428250/464681CF59770BFD493A0D672C7CB70BA8CD3499/\"\n },\n {\n Name = \"VII - Black Stars Rise 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428431/4766E1A4893E0EF16B576749B73CE449F10DA20C/\"\n },\n {\n Name = \"VII - Black Stars Rise 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428896/05ED3D08F29C8E6EC080F0615DE130F2A687A9AC/\"\n },\n {\n Name = \"VIII - Dim Carcosa 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725429052/BA81DBDE7A12B993CB8B7001CA93095AD0512449/\"\n },\n {\n Name = \"VIII - Dim Carcosa 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725429235/C5C939E0213575B5EDCA6723D209892071DDE4B5/\"\n }\n },\n [\"The Forgotten Age\"] = {\n {\n Name = \"I - The Untamed Wilds 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725446729/7F5E48BC9A028AE5117231364F2D5B433A62239A/\"\n },\n {\n Name = \"I - The Untamed Wilds 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725446946/55C374DC5BE3620CB0D5BCA379EA55CF471D09B3/\"\n },\n {\n Name = \"I - The Untamed Wilds 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447172/6EAE2CFD3AC552CFB744C75E4E26A79264DE17D3/\"\n },\n {\n Name = \"I - The Untamed Wilds 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447409/FCF48C94D90F94FBFFD674B0650E288E7312C441/\"\n },\n {\n Name = \"I - The Untamed Wilds 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447579/F7C3FB9E31147430C1384684887FC66E616D0302/\"\n },\n {\n Name = \"II - The Doom of Eztli 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447752/44EF68E1BF3C2D9DCD5306DD90B4CCCFBE03891C/\"\n },\n {\n Name = \"II - The Doom of Eztli 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448027/F5F14EC590C33BC87D8F0CFF43D7E4DF80D61133/\"\n },\n {\n Name = \"II - The Doom of Eztli 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448215/BFD9D940DB8EC9AC8D818C48BBAE3338A8E48A3B/\"\n },\n {\n Name = \"III - Threads of Fate\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448393/67F3BE7DC1BEC807A17D3F8914328D408856E019/\"\n },\n {\n Name = \"IV - The Boundary Beyond 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448568/3C268415A47430CEEC3F9BFB216EF57E3DBF820A/\"\n },\n {\n Name = \"IV - The Boundary Beyond 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448701/3FA23DCA30505AD4C27D8914740AD3138C01122C/\"\n },\n {\n Name = \"IV - The Boundary Beyond 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448863/266C3320AC326C30F08FBC11A95567E1249E7FB7/\"\n },\n {\n Name = \"V - Heart of the Elders 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449019/A6CC459ACA004C33DD9605BE8382437F6BBE24F7/\"\n },\n {\n Name = \"V - Heart of the Elders 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449150/241FA88572B6B4F652D7FC6D1EDB23DF94E3199C/\"\n },\n {\n Name = \"V - Heart of the Elders 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449310/DBD731498FAB399A4155F7B64F8710BF5FBB5C1F/\"\n },\n {\n Name = \"VI - The City of Archives 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449473/527CB6470F508D4E1F1E4AFC8A0D3AB0A65E046E/\"\n },\n {\n Name = \"VI - The City of Archives 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449630/A8E933F79A646990155CC18A2143B3BC16222750/\"\n },\n {\n Name = \"VI - The City of Archives 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449814/30BB7E4F8D9F1571A420372E78ACAF2D7792811F/\"\n },\n {\n Name = \"VII - The Depths of Yoth 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449962/D69532827DFF2D654F8A606B7917D593F52D7624/\"\n },\n {\n Name = \"VII - The Depths of Yoth 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450085/479A8A1BDD7BE80B43AE4F2C14DFA811D7B28482/\"\n },\n {\n Name = \"VII - The Depths of Yoth 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450258/E617F5A78DFBE913652E20BF97D38B91087FACAE/\"\n },\n {\n Name = \"VIII - Shattered Aeons 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450415/5631152D4830C00DB8D8EB2163CE773542A62C79/\"\n },\n {\n Name = \"VIII - Shattered Aeons 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450556/C3000D020B0C32B4DA3420AEE5E286893453FC49/\"\n }\n },\n [\"The Circle Undone\"] = {\n {\n Name = \"0 - Disappearance at the Twilight Estate\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725457506/6E228F87CEDCD3A0CFA28B680C266B4C68C7682B/\"\n },\n {\n Name = \"I - The Witching Hour\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725457706/D0FDC0E9287C343745AE6135352194D654D98B64/\"\n },\n {\n Name = \"II - At Death's Doorstep 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725457864/3106851EC93B1FC01311BD68F02145BA2FD720B0/\"\n },\n {\n Name = \"II - At Death's Doorstep 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458037/155EB572CF28F09A64A021CAC3A3219C31B2CD49/\"\n },\n {\n Name = \"II - At Death's Doorstep 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458187/482CC6730F267061A055D6FD402603BC642A9635/\"\n },\n {\n Name = \"III - The Secret Name 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458373/769D67103FACFB94E77465E85DBB2A250284B59B/\"\n },\n {\n Name = \"III - The Secret Name 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458592/D70CE9B1AD7158AF206A25777A8BA6F284587A83/\"\n },\n {\n Name = \"III - The Secret Name 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458848/2FC0D5AFFA97AB5E80244F1ED711036B2149B0EE/\"\n },\n {\n Name = \"III - The Secret Name 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459026/A1A5B18ECB38985A35781CBDFF53935541720AF4/\"\n },\n {\n Name = \"IV - The Wages of Sin 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459182/46870E1DBE623E93E675CB2D3C1E959B40CDCC83/\"\n },\n {\n Name = \"IV - The Wages of Sin 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459344/9552699A8CEC5E7BF22843990394BB977539CBE0/\"\n },\n {\n Name = \"IV - The Wages of Sin 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459466/9B6B55A6F76668929B6D6B1DD5FDAE7CEE427A1C/\"\n },\n {\n Name = \"IV - The Wages of Sin 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459609/054A60BDCD266C1A53F29067CFC83D0561666FE4/\"\n },\n {\n Name = \"IV - Wages of Sin 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459714/11B3D734AC5229A0DEA5372CD50F5C7841F9D5A0/\"\n },\n {\n Name = \"V - For the Greater Good 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459829/9AA9563CCD72A8593BDE3C6D1299E96325ECE747/\"\n },\n {\n Name = \"V - For the Greater Good 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459933/13A103B7BC0ABB611F6A8E61D033C6AD95EED4D4/\"\n },\n {\n Name = \"V - For the Greater Good 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460088/C88DDC5CE898ACEF360B8AD72E05F15BF84DE171/\"\n },\n {\n Name = \"V - For the Greater Good 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460182/C086EDA78636624FC9C4EA1DBCD8F1D39B7A6A89/\"\n },\n {\n Name = \"VI - Union and Disillusion\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460382/8EFB842A623A2D1E6D0E915268A207A5D7DFC6D4/\"\n },\n {\n Name = \"VII - In the Clutches of Chaos 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460586/7E156BF93F211BC425CD37ED273FCC84FF1F4C4D/\"\n },\n {\n Name = \"VII - In the Clutches of Chaos 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460740/72A39A0FACA806EF2E0A4E31AEDF6D8FCDF1A876/\"\n },\n {\n Name = \"VII - In the Clutches of Chaos 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460884/5716F6E2CB3F8D14FA5B7B806555896088696057/\"\n },\n {\n Name = \"VIII - Before the Black Throne 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725461042/DFF7A992A9440E58CE32D4B8A96A02A8F785AC90/\"\n },\n {\n Name = \"VIII - Before the Black Throne 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725461161/18902B2F74C9D70F5EA79A34521E3FDEDEFC893D/\"\n }\n },\n [\"The Dream-Eaters\"] = {\n {\n Name = \"I-A - Beyond the Gates of Sleep 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725465733/828EE1FF16928EA0426BC68AAED4B9AA6B6FBB49/\"\n },\n {\n Name = \"I-A - Beyond the Gates of Sleep 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725465934/35A85E9056B7A212AC39E7043FDF546A425E62F2/\"\n },\n {\n Name = \"I-A - Beyond the Gates of Sleep 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466105/05D29313D2559B6683465A7034DE7B07DE675420/\"\n },\n {\n Name = \"I-B - Waking Nightmare\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466267/16FAE20612ED0A6A65836DD1B014AC5A801A172F/\"\n },\n {\n Name = \"II-A - The Search for Kadath 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466425/03CCD5F24999CC6CC905CB2FD021DB67D7769163/\"\n },\n {\n Name = \"II-A - The Search for Kadath 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466615/085ACE95FEF03D2BE91BAD5E1864E1D0263DCCC0/\"\n },\n {\n Name = \"II-A - The Search for Kadath 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466802/83861733568FCA9673B6F5EFE0C9D3337E3DFD27/\"\n },\n {\n Name = \"II-A - The Search for Kadath 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466941/D4FDE1FE722DC6B03AD38D797F2E00531908583D/\"\n },\n {\n Name = \"II-A - The Search for Kadath 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467396/C9A476395B9326142BBDBD06D22A14C83EAD2161/\"\n },\n {\n Name = \"II-B - A Thousand Shapes of Horror 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467582/5EB69F3A3F20B312FE3FE3C64FEA96200EE51563/\"\n },\n {\n Name = \"II-B - A Thousand Shapes of Horror 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467791/5FE437C821641EDD703D336205A37F8F0CACFD38/\"\n },\n {\n Name = \"II-B - A Thousand Shapes of Horror 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467946/841CEC1D8B56C1CA52B5558E8E49CE04D95D2F4A/\"\n },\n {\n Name = \"III-A - Dark Side of the Moon 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468100/2DD1038163AD5DAA759EA4BE49DB82A2718E931F/\"\n },\n {\n Name = \"III-A - Dark Side of the Moon 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468293/E8145D05833864EEC70AF9AC401039D8E8AFDE3E/\"\n },\n {\n Name = \"III-B - Point of No Return 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468506/1F2E3D425A4D97ADBCA59B4A9401C411F91F875F/\"\n },\n {\n Name = \"III-B - Point of No Return 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468694/8631E68BA7CE69BECF1D476A03CBDC79112A60BF/\"\n },\n {\n Name = \"IV-A - Where the Gods Dwell\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468880/2C8A9459149D33E526FDC4B68A063475305C15F8/\"\n },\n {\n Name = \"IV-B - Weaver of the Cosmos 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725469060/9E6AF9E0D68EC0F44B82968CE99E433A25A0E0C4/\"\n },\n {\n Name = \"IV-B - Weaver of the Cosmos 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725469224/8C969B8ABAEE6CFFEF165C2E4BBA0E7E9B33AA1A/\"\n },\n {\n Name = \"IV-B - Weaver of the Cosmos 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725469417/A94C14A64836FB9B5FAFAC5B790C51441F8D475F/\"\n }\n },\n [\"The Innsmouth Conspiracy\"] = {\n {\n Name = \"I - The Pit of Despair 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473070/57FEAA8135F62DBAD52E2AAB562EF45EB9A0194A/\"\n },\n {\n Name = \"I - The Pit of Despair 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473257/FEC6F9FAA1CC656BFD5861F58E4751BC94AB0424/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473424/B0E2BFA9C7F61A5B7A72CD11AD1AFF4A070AFFA1/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473570/D28C2DA099D656116A0D7BAA8B874E4ED6B6B50E/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473731/EE9BED3522A1B7CD5E394A67AD7F692DB709A9B5/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473874/0A85B2CE9A3FFB048C932E1B014B2986D5D42A1B/\"\n },\n {\n Name = \"II - Vanishing of Elina Harper 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474056/C82E467216DF747AA954C614D7D0B8F649163EDA/\"\n },\n {\n Name = \"III - In Too Deep 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474234/C6A72CDEFCDF1F2A3C6036F349B5F368EFDACA2E/\"\n },\n {\n Name = \"III - In Too Deep 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474398/752D0CE313C58A5B1C3181503C841EB91D041492/\"\n },\n {\n Name = \"III - In Too Deep 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474562/308687DFB9B0F2286A9248698FD38C0BF66B89ED/\"\n },\n {\n Name = \"IV - Devil Reef 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474717/DCEC44D2F7F22A950B3CF52C81C8F931710EFADC/\"\n },\n {\n Name = \"IV - Devil Reef 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474883/04913E71194BF766DFF2DF8646251AC461A35FE2/\"\n },\n {\n Name = \"V - Horror in High Gear 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475029/FAA34C92FA800E8DDF6449B1C48FCC7468D783BD/\"\n },\n {\n Name = \"V - Horror in High Gear 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475133/BC689B58040F74E7DB44CFD8F3F2BF32FE4081E1/\"\n },\n {\n Name = \"V - Horror in High Gear 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475307/53ADF5E957F88F1DDCE46828547E5808C3CAEFD7/\"\n },\n {\n Name = \"V - Horror in High Gear 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475402/C41EB1D2F8549669DD4323A339F5EC594996816C/\"\n },\n {\n Name = \"VI - A Light in the Fog 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475509/C526C9A3AC34195ACF6161D4821D1424A8A288B4/\"\n },\n {\n Name = \"VI - A Light in the Fog 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475660/2C67D57967F300BD1FE1AFC08C49EBCF4A88D11C/\"\n },\n {\n Name = \"VII - The Lair of Dagon 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476055/4BC221648012F7A7A37B2F65929705E973FF9CE3/\"\n },\n {\n Name = \"VII - The Lair of Dagon 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476271/3F9CF0FE8ACCE12E26ACA5D5777177A10B49D6DA/\"\n },\n {\n Name = \"VIII - Into the Maelstrom 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476474/AF8695342FBCE6F6BC7B353C86C6006F1629F43A/\"\n },\n {\n Name = \"VIII - Into the Maelstrom 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476643/CB175401ECDF04F97334CBF8AE7EC76A43A85735/\"\n }\n },\n [\"Edge of the Earth\"] = {\n {\n Name = \"I - Ice and Death 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482152/322E07021DC78455859C860D8B1574F1BA2E0F68/\"\n },\n {\n Name = \"I - Ice and Death 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482327/BD14DDE856A2062A939F860743FFF15F8B0BFF5A/\"\n },\n {\n Name = \"I - Ice and Death 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482520/0FB5AC83DF448994EC0A866ACF6AF57ADCC59C82/\"\n },\n {\n Name = \"??? - Fatal Mirage\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725481929/A35EDF11BD6AF52784BA6611C363CBBB373622EE/\"\n },\n {\n Name = \"II - To the Forbidden Peaks 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482688/B50455616DFC14FE0B9398DBE2A3A1AE25040516/\"\n },\n {\n Name = \"II - To the Forbidden Peaks 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482839/17786225AA56E75E558491E7E710F555AF3E5799/\"\n },\n {\n Name = \"III - City of the Elder Things 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483014/B74378E02A1A99F544CD98141EF62193A2A612FB/\"\n },\n {\n Name = \"III - City of the Elder Things 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483208/CDFF7A2D404485DE2B4C4ED54B34E197515F0094/\"\n },\n {\n Name = \"IV - The Heart of Madness 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483349/A96BC4AA73C71FE86EF38110236BAEF710C00EE2/\"\n },\n {\n Name = \"IV - The Heart of Madness 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483528/5BF0DD740C5D43EFBFEF0436EB35FF9DA748FA5E/\"\n }\n },\n [\"The Scarlet Keys\"] = {\n {\n Name = \"5-A Riddles and Rain\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792057358580/E9E5FE4028C08B3D4883406821221B73C8B5B2C7/\"\n },\n {\n Name = \"11-B Dead Heat\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566443853/CAD7771D90141EA6D5FFAFE1EC5E7AD9647C82DB/\"\n },\n {\n Name = \"16-D Sanguine Shadows\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792057358704/4A7261EB31511467CBC46E876476DD205F528A4B/\"\n },\n {\n Name = \"21-F Dealings in the Dark\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792057358816/7C9FE4C34CD0A7AE87EF054742D878F310C71AA7/\"\n },\n {\n Name = \"28-I Dancing Mad\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792056955518/EAB857DD5629EC6A3078FB0A3A703B85B5F514B9/\"\n },\n {\n Name = \"23-K On Thin Ice\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444026/EB5628E254AE25DA89A9C999EAAD995ECF67068E/\"\n },\n {\n Name = \"38-N Dogs of War\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444199/194FD9A713907197471A55411AE300B62C5F5278/\"\n },\n {\n Name = \"46-Q Shades of Suffering\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444330/3ED2CCE95DE933546E1B5CBBF445D773E6D65465/\"\n },\n {\n Name = \"56-Y Without a Trace\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444450/FE4C335B0F72E83900A4EED0FD1A1D304D70D6B7/\"\n },\n {\n Name = \"59-Z Congress of the Keys 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444576/5BB32469ED412D59BB0A46E57D226500B1D0568B/\"\n },\n {\n Name = \"59-Z Congress of the Keys 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444690/B01A1FEAB57473D9B6DF11B92D62C214AA1C2C02/\"\n }\n }\n },\n [\"Official Scenarios\"] = {\n [\"The Blob That Ate Everything\"] = {\n {\n Name = \"The Blob That Ate Everything 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489144/53A920E2D1A9F41937B21B0A5B1A4E450ABFC460/\"\n },\n {\n Name = \"The Blob That Ate Everything 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725488396/9CC07F923BD1CBFC4BB06DD2CC6747D2C3541737/\"\n }\n },\n [\"Carnevale of Horrors\"] = {\n {\n Name = \"Carnevale of Horrors 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725496678/91164AF7C2ED06A3E50225794DE9C5E92D1D3B04/\"\n },\n },\n [\"Curse of the Rougarou\"] = {\n {\n Name = \"Curse of the Rougarou 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725494513/DF8E92EA5C1131A0C5D8FA06BFF6FD239412E11C/\"\n },\n {\n Name = \"Curse of the Rougarou 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725494706/5D2EDE5D07DDA6E5781DB70D13F9A17EF26D36F2/\"\n },\n {\n Name = \"Curse of the Rougarou 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725494855/44A751CB66CBE4D75E67A53FC392696F6A3BFD4C/\"\n },\n {\n Name = \"Curse of the Rougarou 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725495011/ADD3431B383236BCF057C1573CEF7E43F39FDE72/\"\n },\n {\n Name = \"Curse of the Rougarou 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725495146/79159992D5002AD3D7D23ED82D9BB76D437B94C3/\"\n }\n },\n [\"Guardians of the Abyss\"] = {\n {\n Name = \"Guardians of the Abyss 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725490870/9C03B2974D7776B8E2AFEEF025756D1885AF0AE3/\"\n },\n {\n Name = \"Guardians of the Abyss 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491039/E81F05AB9C802F57F4D9F7229CE3D53231ACB70D/\"\n },\n {\n Name = \"Guardians of the Abyss 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491202/384AA7DBA614B61C46D57EF5105A96F25F938B74/\"\n },\n {\n Name = \"Guardians of the Abyss 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491392/1C8FF752DF97F362BA8937BFEC65140AE3ADF8A6/\"\n },\n {\n Name = \"Guardians of the Abyss 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491512/06B1DCA10B44FF86567CEFCC135D619648CE5F19/\"\n },\n {\n Name = \"Guardians of the Abyss 6\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491658/5446E231AEF72CF4F7B4892116671FF7175EFA0F/\"\n }\n },\n [\"The Labyrinths of Lunacy\"] = {\n {\n Name = \"The Labyrinths of Lunacy 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489685/D2D342844212C8A21E030418935A227C2E3279DB/\"\n },\n {\n Name = \"The Labyrinths of Lunacy 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489820/E3E18B0940C2604F62E564AD43F178FF9F13B3C9/\"\n },\n {\n Name = \"The Labyrinths of Lunacy 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489972/6A34CF53190EAAAF57C31FB97A3C2ACBD27FEE40/\"\n }\n },\n [\"Murder at the Excelsior Hotel\"] = {\n {\n Name = \"Murder at the Excelsior Hotel 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725488868/7F7FE8BB3C7E3645B4377F86366C6073CDB8F113/\"\n },\n {\n Name = \"Murder at the Excelsior Hotel 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489144/53A920E2D1A9F41937B21B0A5B1A4E450ABFC460/\"\n }\n },\n [\"War of the Outer Gods\"] = {\n {\n Name = \"War of the Outer Gods\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725487627/43A19A6D97A6DA63A487EB247EE95884E2D9F5FD/\"\n }\n }\n },\n [\"Fan-Made Campaigns\"] = {\n [\"Cyclopean Foundations\"] = {\n {\n Name = \"I - Lost Moorings 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725499518/02DA17FC9D6A1174E484977269F44AE6995C6F7C/\"\n },\n {\n Name = \"I - Lost Moorings 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725499680/1556F230B59D42FCA47A5A87135330B03C231E92/\"\n },\n {\n Name = \"II - Going Twice\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725499855/1220C81273EFA6F36C2AEBA713E31DE1E2F92454/\"\n },\n {\n Name = \"III - Private Lives\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500069/452E4427185A7F1AFB3F2CBB263DC55B6A144D49/\"\n },\n {\n Name = \"IV - Crumbling Masonry 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500260/94FE8506AB13CA51F087ABF155799F73BCBAB1E9/\"\n },\n {\n Name = \"IV - Crumbling Masonry 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500508/04B6C6D8845BF5411D28C79D185EAF6FFBB409F5/\"\n },\n {\n Name = \"V - Across Dreadful Waters\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500640/447069BBBF70474439D3CC4F970F6617C8B738F4/\"\n },\n {\n Name = \"VI - Blood From Stones\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500803/23C46C5F08E3F911F565D9EC38CFACFFAA5F5B11/\"\n },\n {\n Name = \"VII - Pyroclastic Flow 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500952/804B531E517B1AD2294E304AA6F72F8CC3E6FC4E/\"\n },\n {\n Name = \"VII - Pyroclastic Flow 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501115/E771289EB848A695DF47C405300B1EC7CA925009/\"\n },\n {\n Name = \"VIII - Tomb of Dead Dreams 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501272/CAE6C4185540F3F8FFA2A8E37458A5B80DBD70FC/\"\n },\n {\n Name = \"VIII - Tomb of Dead Dreams 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501415/DA9C07488DA2753FF5621D1731A84D6D0B1CC1BD/\"\n },\n {\n Name = \"VIII - Tomb of Dead Dreams 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501813/030E5DB9F8F6C5F092EFDF2C42AD631318B0923A/\"\n }\n },\n [\"Dark Matter\"] = {\n {\n Name = \"I - The Tatterdemalion 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504118/AC852F478D5BDA0C8A54A499B07A66E872560EC7/\"\n },\n {\n Name = \"I - The Tatterdemalion 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504319/5B24BB2080AC76D836708AABC1BC90FD884F043D/\"\n },\n {\n Name = \"I - The Tatterdemalion 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504461/73E4632A2EAAFA918924E60A64B03838CA6DDD77/\"\n },\n {\n Name = \"I - The Tatterdemalion 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504602/6DB8F081CC907A0D6F364E5045BB7E8FADA91B5C/\"\n },\n {\n Name = \"II - Electric Nightmares 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504770/C6FB5FDD153ACD07565259AE013FDC7FF567037D/\"\n },\n {\n Name = \"II - Electric Nightmares 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504974/80E29EA68B88B78CDADC475998E832C7245409F4/\"\n },\n {\n Name = \"IIIa - Lost Quantum\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725505211/07E1254B1C601496E47B7E60B736D1699AAD38C8/\"\n },\n {\n Name = \"IIIb - In the Shadow of Earth 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725505419/7E8B50470AD6AD27429B58A42271755289FC90EB/\"\n },\n {\n Name = \"IIIb - In the Shadow of Earth 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725505603/DCD5B255FE66F8D8F47FB6C928D80583F3F950AC/\"\n },\n {\n Name = \"IIIc - Strange Moons\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506071/78597C2FBE2DE55BFC86AEC9F42FE1B20D26544C/\"\n },\n {\n Name = \"IV - The Machine in Yellow\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426327/41F6192EDCFFD6AAE2EE44C2BB5708B19D7464A9/\"\n },\n {\n Name = \"V - Fragment of Carcosa 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506388/A299C8D58171A6D78CF55D911C2B81C63D88444F/\"\n },\n {\n Name = \"V - Fragment of Carcosa 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506567/C36509CB6520803355E7AE1E9EC8CD35641B28C8/\"\n },\n {\n Name = \"VI - Starfall 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506714/327D0E565039B7FA9157FDA04A302DF178C64C44/\"\n },\n {\n Name = \"VI - Starfall 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506809/E637DAAD8BEB00E4305756D101E1E6B370CB1644/\"\n },\n {\n Name = \"VI - Starfall 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506978/A17CD45A7770BAC7196218ADD2FB6CEB0E7A0E6B/\"\n }\n },\n [\"The Ghosts of Onigawa\"] = {\n {\n Name = \"I - The Hidden Village\",\n URL = \"https://i.imgur.com/btTQffc.jpeg\"\n },\n {\n Name = \"II - The House on the Hill\",\n URL = \"https://i.imgur.com/YTHt8JQ.jpeg\"\n },\n {\n Name = \"III - The River Delta\",\n URL = \"https://i.imgur.com/9Zk3iLJ.png\"\n },\n {\n Name = \"IV - The War Eternal\",\n URL = \"https://i.imgur.com/6UtFHhc.jpeg\"\n },\n {\n Name = \"V - Half Light\",\n URL = \"https://i.imgur.com/hul7lLL.png\"\n },\n {\n Name = \"VI - The End of August\",\n URL = \"https://i.imgur.com/PKWtpG7.jpeg\"\n },\n {\n Name = \"VII - The Molten Armory\",\n URL = \"https://i.imgur.com/kMSdBRh.jpeg\"\n },\n {\n Name = \"VIII - The Black Harvest\",\n URL = \"https://i.imgur.com/6ySucTS.jpeg\"\n }\n }\n },\n [\"Fan-Made Scenarios\"] = {\n [\"Side Scenarios (FM)\"] = {\n {\n Name = \"Code Red at Bleeding Heart\",\n URL = \"https://i.imgur.com/nTstdlj.jpeg\"\n },\n {\n Name = \"Consternation on the Constellation\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725512402/37F34A14CEEA9D2F889F7B97B065C0193F268FE1/\"\n },\n {\n Name = \"Symphony of Erich Zann\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725512613/B23EA91E9489E0DDE250DD33F9AF1A12EEE52E0C/\"\n }\n }\n },\n [\"Other Images\"] = {\n [\"Arkham Locations\"] = {\n {\n Name = \"Downtown 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516086/53F48F8AA9CFE4BF544BF03A616AC12A5344615C/\"\n },\n {\n Name = \"Downtown 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516316/1A337F5D66D7B5F59C66F465710717340B1D56AB/\"\n },\n {\n Name = \"Eastside 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516542/09B4BE1E5487C3B11C0178B0B6FFD51620BE6AA6/\"\n },\n {\n Name = \"Eastside 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516744/9B7F54E99D9B85884D5E363B97B25DA2DD3F03CC/\"\n },\n {\n Name = \"French Hill\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516949/8BFEF09FDB6608173280F887C1BA3906427678CC/\"\n },\n {\n Name = \"Generic 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517086/93E00AA823FBA1C6531BBA143408E1E7D89BE3F0/\"\n },\n {\n Name = \"Generic 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517270/1450BCB6698058F833FAE770D31D0D064B82F5C2/\"\n },\n {\n Name = \"Generic 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517434/5B529D9DEF3550E898744CE19AAD4CC0AB3F12DF/\"\n },\n {\n Name = \"Generic 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517622/FE0C331881B776E66D3A2C60D70147A0CABFDAC6/\"\n },\n {\n Name = \"Generic 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517833/A123F1A2D1B8BE960AAB371D9FD06F27C282CB9B/\"\n },\n {\n Name = \"Generic 6\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518026/10D4F06C8A1AEC3ACAEC5C3B9DA142D3BA5818FE/\"\n },\n {\n Name = \"Generic 7\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518144/72D4A60264C2DD201AAA5FFDCA95C6FF04EF8AC8/\"\n },\n {\n Name = \"Generic 8\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518292/A1848A68389FBECA5BC847F90A921C18133B17D6/\"\n },\n {\n Name = \"Merchant District\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518454/BD6694DCCD12E31997202B2020B4A29FDC96FC7B/\"\n },\n {\n Name = \"Miskatonic University\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518846/79D6BEB0D8C274E3D308A67CD3181418B02D3A7E/\"\n },\n {\n Name = \"Northside\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519012/E4B527768AFEFE0E4FB4518CA2AFDD69A98AB5D1/\"\n },\n {\n Name = \"Rivertown\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519190/7A9A09861EECE2D98B5C1EB694E88C2073ABDFDF/\"\n },\n {\n Name = \"Southside\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519353/FE43349A25231F48662309FEB331DB2C418A5E80/\"\n },\n {\n Name = \"Uptown\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519551/3630457608AD2DB605EC4BCF4332C1D0983305C0/\"\n }\n },\n [\"Default Image\"] = {\n {\n Name = \"Default Image\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/\"\n }\n },\n [\"Unsorted\"] = {\n {\n Name = \"Kingsport\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725522594/1EA2C0AF5D4D346AD3FFDC38215BB20AAA72CE8D/\"\n },\n {\n Name = \"Devil\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2115062479282248687/DD84A3CB3C4A475A5D093CB413A16A5CEA5FBF79/\"\n },\n {\n Name = \"Mystic Board\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2115062479282248488/EC27B1215F558A39954C27477D8B4F916CA211E5/\"\n }\n }\n }\n}\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PlayAreaSelector\")\nend)\n__bundle_register(\"core/PlayAreaSelector\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PlayAreaImageData\") -- this fills the variable \"PLAYAREA_IMAGE_DATA\"\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal typeIndex, selectionIndex, plainNameCache\n\nfunction onSave()\n return JSON.encode({\n typeIndex = typeIndex,\n selectionIndex = selectionIndex\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData) or {}\n typeIndex = loadedData.typeIndex or 1\n selectionIndex = loadedData.selectionIndex or 1\n end\n\n self.createButton({\n function_owner = self,\n click_function = \"onClick_toggleGallery\",\n tooltip = \"Show Image Gallery\",\n position = {0, 0.06, 0},\n height = 1500,\n width = 1500,\n color = { 1, 1, 1, 0 }\n })\n\n Wait.time(updatePlayAreaGallery, 0.5)\n math.randomseed(os.time())\nend\n\n-- click function for main button\nfunction onClick_toggleGallery(_, playerColor)\n Global.call(\"togglePlayAreaGallery\", playerColor)\nend\n\nfunction getDataSubTableByIndex(dataTable, index)\n local loopId = 1\n for i, v in pairs(dataTable) do\n if index == loopId then return v end\n loopId = loopId + 1\n end\n return {}\nend\n\nfunction updatePlayAreaGallery()\n -- get subtables\n local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex)\n local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex)\n\n -- get global xml to insert elements\n local globalXml = UI.getXmlTable()\n\n -- selectable items\n local itemSelection = getXmlTableElementById(globalXml, 'itemSelection')\n itemSelection.children = {}\n\n local i = 0\n for itemName, _ in pairs(dataForType) do\n i = i + 1\n table.insert(itemSelection.children,\n {\n tag = \"Panel\",\n attributes = { class = \"itemPanel\", id = \"typePanel\" .. i },\n children = {\n tag = \"Text\",\n value = itemName,\n attributes = { class = \"itemText\", id = \"typeListText\" .. i }\n }\n })\n end\n\n -- selectable images for that item\n local playareaList = getXmlTableElementById(globalXml, 'playareaList')\n playareaList.children = {}\n\n for i, v in ipairs(dataForSelection) do\n table.insert(playareaList.children,\n {\n tag = \"VerticalLayout\",\n attributes = { class = \"imageBox\", id = \"image\" .. i },\n children = {\n {\n tag = 'Image',\n attributes = { class = \"playareaImage\", image = v.URL }\n },\n {\n tag = 'Text',\n value = v.Name,\n attributes = { class = \"imageName\" }\n }\n }\n })\n end\n\n playareaList.attributes.height = round(#playareaList.children / 2, 0) * 380\n Global.call(\"updateGlobalXml\", globalXml)\n Wait.time(highlightTabAndItem, 0.1)\nend\n\nfunction onClick_imageTab(_, _, tabId)\n typeIndex = tonumber(tabId:sub(9))\n selectionIndex = 1\n updatePlayAreaGallery()\nend\n\nfunction onClick_listItem(_, _, listId)\n selectionIndex = tonumber(listId:sub(10))\n updatePlayAreaGallery()\nend\n\nfunction onClick_image(player, _, id)\n local imageIndex = tonumber(id:sub(6))\n local dataForType = getDataSubTableByIndex(PLAYAREA_IMAGE_DATA, typeIndex)\n local dataForSelection = getDataSubTableByIndex(dataForType, selectionIndex)\n local newURL = dataForSelection[imageIndex].URL\n playAreaApi.updateSurface(newURL)\n Global.call(\"togglePlayAreaGallery\", player.color)\nend\n\nfunction highlightTabAndItem()\n -- highlight active tab\n for i = 1, 5 do\n local color = \"#888888\"\n if i == typeIndex then color = \"#ffffff\" end\n UI.setAttribute(\"imageTab\" .. i, \"color\", color)\n end\n\n -- highlight item\n UI.setAttribute(\"typePanel\" .. selectionIndex, \"color\", \"grey\")\n UI.setAttribute(\"typeListText\" .. selectionIndex, \"color\", \"black\")\nend\n\n-- loops through an XML table and returns the specified object\n---@param ui table XmlTable (get this via getXmlTable)\n---@param id string Id of the object to return\nfunction getXmlTableElementById(ui, id)\n for _, obj in ipairs(ui) do\n if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end\n if obj.children then\n local result = getXmlTableElementById(obj.children, id)\n if result then return result end\n end\n end\n return nil\nend\n\n-- utility function\nfunction round(num, numDecimalPlaces)\n local mult = 10 ^ (numDecimalPlaces or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\nfunction maybeUpdatePlayAreaImage(scenarioName)\n -- check if option is enabled\n local optionPanelState = optionPanelApi.getOptions()\n if not optionPanelState[\"changePlayAreaImage\"] then return end\n\n -- initialize cache if nil\n if not plainNameCache then\n plainNameCache = {}\n for i, dataForType in pairs(PLAYAREA_IMAGE_DATA) do\n for j, dataForCycle in pairs(dataForType) do\n for k, data in ipairs(dataForCycle) do\n local plainName = getPlainName(data.Name)\n \n -- override plainName for all images in the \"Other Images\" category (except the default image)\n if i == \"Other Images\" and data.Name ~= \"Default Image\" then\n plainName = \"Generic\"\n end\n\n if not plainNameCache[plainName] then\n plainNameCache[plainName] = {}\n end\n table.insert(plainNameCache[plainName], data.URL)\n end\n end\n end\n end\n\n -- look for matching playarea image or use generic ones instead\n local listOfEligibleImages = {}\n if plainNameCache[scenarioName] then\n listOfEligibleImages = plainNameCache[scenarioName]\n else\n listOfEligibleImages = plainNameCache[\"Generic\"]\n end\n\n -- get a random entry from the eligible list\n local newImageIndex = math.random(#listOfEligibleImages)\n playAreaApi.updateSurface(listOfEligibleImages[newImageIndex])\nend\n\n-- attempts to extract the plain scenario name from the playarea image name\nfunction getPlainName(str)\n -- remove prefix type 1\n str = str:gsub(\"%w+%-%w%s%-%s\", \"\") -- matches \"II-B - Thousand Shapes of Horror 1\"\n\n -- remove prefix type 2\n str = str:gsub(\"%w+%-%w%s\", \"\") -- matches \"59-Z Congress of Keys 1\"\n\n -- remove prefix type 3\n str = str:gsub(\"%w+%s%-%s\", \"\") -- matches \"III - The Secret Name 4\"\n\n -- remove prefix type 4\n str = str:gsub(\"%?+%s%-%s\", \"\") -- matches \"??? - Fatal Mirage\"\n\n -- remove suffix (numbering)\n str = str:gsub(\"%s%d+\", \"\")\n\n return str\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/PlayAreaImageData\", function(require, _LOADED, __bundle_register, __bundle_modules)\nPLAYAREA_IMAGE_DATA = {\n [\"Official Campaigns\"] = {\n [\"Night of the Zealot\"] = {\n {\n Name = \"I - The Gathering 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725443007/D34B55D2637EF1DF22839D12F9CF74F92F8EB486/\"\n },\n {\n Name = \"III - The Devourer Below 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725443203/FBE04C8B89F79D18C6D29C28DC3B292A5A3A3DEB/\"\n },\n {\n Name = \"III - The Devourer Below 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725443345/FD10BC04B3F3AFD710C3C12EE14F85F9AFB265E6/\"\n }\n },\n [\"The Dunwich Legacy\"] = {\n {\n Name = \"I-A - Extracurricular Activity 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725401398/AC55F0754B7E4A39796A6F2236012AE03DA55E20/\"\n },\n {\n Name = \"I-A - Extracurricular Activity 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725401658/9F3ED6E256818C4528D55980B9D7E44B87170A4F/\"\n },\n {\n Name = \"I-A - Extracurricular Activity 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725401870/FE29404D9D93BE2735D41C115DDD9708A0931F3E/\"\n },\n {\n Name = \"I-B - The House Always Wins 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402059/4C4A50D259ECFD4DFB94FABB9FA2AD3AABDCA0CA/\"\n },\n {\n Name = \"I-B - The House Always Wins 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402263/8B0C981B78E803B3BFD64AC874F315B6810E579B/\"\n },\n {\n Name = \"I-B - The House Always Wins 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402460/8599D17306C644D75D189591BC6D57C2F27F9E35/\"\n },\n {\n Name = \"I-B - The House Always Wins 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402641/777865B25BCF86C2EFEAE232AFBC31561D12AE0F/\"\n },\n {\n Name = \"II - The Miskatonic Museum 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725402804/A44E68AC08E6568A757429B29E71C703B76FA159/\"\n },\n {\n Name = \"II - The Miskatonic Museum 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403000/B28D46D5B7BCE9EA9BF27D3A0A932393D8258A76/\"\n },\n {\n Name = \"III - The Essex County Express\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403150/956BE4B5C25805E57C8FC5CF28A94BD7EF07CA54/\"\n },\n {\n Name = \"IV - Blood on the Altar 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403290/07DD1508BA880AD1212A820BC29471C48DC90C53/\"\n },\n {\n Name = \"IV - Blood on the Altar 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403456/A74398E8CCD78657C2555832A3B340723E8C9117/\"\n },\n {\n Name = \"IV - Blood on the Altar 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403636/CD89D7D9CC020F41D5AD02D54E308877583EC45F/\"\n },\n {\n Name = \"IV - Blood on the Altar 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403818/AEFAC50DAC82271BCACC53B8FF69B5B094FA1078/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725403955/E7C566E536CF6E910DB654FC1DBF4838F2BAF899/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404127/F92F123BBD3D857C579B9284988C3AFFBCD84B2B/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404273/7F8CFE6785BD74BB8D5E34C204AFB8AA580CA3E3/\"\n },\n {\n Name = \"V - Undimensioned and Unseen 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404424/C2AE08371CF2DE2777791BBD4AEF446E50532382/\"\n },\n {\n Name = \"VI - Where Doom Awaits 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404578/A520CE0F6B0C8591A48ADEF34E68B46AFC2BC83B/\"\n },\n {\n Name = \"VI - Where Doom Awaits 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404740/1FF9EF375BDBB73CA8F0A1562F215692AAEA7131/\"\n },\n {\n Name = \"VI - Where Doom Awaits 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725404914/5473D20CE0122F55EEADB16C4353D4EDD91E440E/\"\n },\n {\n Name = \"VI - Where Doom Awaits 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725405088/A0AA8CB864747152763D434D0737D81EB515E71B/\"\n },\n {\n Name = \"VI - Where Doom Awaits 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725405247/FAF408DAAD4FC72142DFDFE5FCEA4B84293AA72C/\"\n },\n {\n Name = \"VII - Lost in Time and Space 1\",\n URL = \"https://i.ibb.co/rtTpbDx/Dunwich-8-Lost-in-Time-amp-Space.jpg\"\n },\n {\n Name = \"VII - Lost in Time and Space 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725405746/771BAE40F98BB16F8D011FA794E4AC0095131AF1/\"\n },\n {\n Name = \"VII - Lost in Time and Space 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725406148/D15D56EA34F27C651D7E7AC202DA4DEBE395E310/\"\n }\n },\n [\"The Path to Carcosa\"] = {\n {\n Name = \"I - Curtain Call\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426327/41F6192EDCFFD6AAE2EE44C2BB5708B19D7464A9/\"\n },\n {\n Name = \"II - The Last King 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426499/114EAEA245AC51CA219364AF26341E7F7E649A7D/\"\n },\n {\n Name = \"II - The Last King 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426683/A2762E95DD79D7A7BC749925166BBFC18B62EF3B/\"\n },\n {\n Name = \"II - The Last King 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426818/A6A20773EA95CE3D9896B22E317A0E1ACCA911F0/\"\n },\n {\n Name = \"III - Echoes of the Past\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427005/25F51DE4B1F33C16A0E68C929E5002F0C4520A37/\"\n },\n {\n Name = \"IV - The Unspeakable Oath 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427178/9575E4F6E53DDAD2D3E61684AB9757B04E1EF787/\"\n },\n {\n Name = \"IV - The Unspeakable Oath 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427342/199A6C6411992ACFB8A417E86207FE6CE956EB97/\"\n },\n {\n Name = \"IV - The Unspeakable Oath 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427501/48525BCDFA281947FA9ED26507BC1F81C07056E8/\"\n },\n {\n Name = \"V - A Phantom of Truth 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427681/F72B02FF1A5E58CBCB0E53C8455310AF37064477/\"\n },\n {\n Name = \"V - A Phantom of Truth 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725427848/215A757FF28317EB3EF7CF1F9CAE6F1CFF735091/\"\n },\n {\n Name = \"VI - The Pallid Mask 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428067/D46BAD80B112156A2D3DEFF247A74C74F27DDA12/\"\n },\n {\n Name = \"VI - The Pallid Mask 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428250/464681CF59770BFD493A0D672C7CB70BA8CD3499/\"\n },\n {\n Name = \"VII - Black Stars Rise 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428431/4766E1A4893E0EF16B576749B73CE449F10DA20C/\"\n },\n {\n Name = \"VII - Black Stars Rise 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725428896/05ED3D08F29C8E6EC080F0615DE130F2A687A9AC/\"\n },\n {\n Name = \"VIII - Dim Carcosa 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725429052/BA81DBDE7A12B993CB8B7001CA93095AD0512449/\"\n },\n {\n Name = \"VIII - Dim Carcosa 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725429235/C5C939E0213575B5EDCA6723D209892071DDE4B5/\"\n }\n },\n [\"The Forgotten Age\"] = {\n {\n Name = \"I - The Untamed Wilds 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725446729/7F5E48BC9A028AE5117231364F2D5B433A62239A/\"\n },\n {\n Name = \"I - The Untamed Wilds 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725446946/55C374DC5BE3620CB0D5BCA379EA55CF471D09B3/\"\n },\n {\n Name = \"I - The Untamed Wilds 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447172/6EAE2CFD3AC552CFB744C75E4E26A79264DE17D3/\"\n },\n {\n Name = \"I - The Untamed Wilds 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447409/FCF48C94D90F94FBFFD674B0650E288E7312C441/\"\n },\n {\n Name = \"I - The Untamed Wilds 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447579/F7C3FB9E31147430C1384684887FC66E616D0302/\"\n },\n {\n Name = \"II - The Doom of Eztli 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725447752/44EF68E1BF3C2D9DCD5306DD90B4CCCFBE03891C/\"\n },\n {\n Name = \"II - The Doom of Eztli 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448027/F5F14EC590C33BC87D8F0CFF43D7E4DF80D61133/\"\n },\n {\n Name = \"II - The Doom of Eztli 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448215/BFD9D940DB8EC9AC8D818C48BBAE3338A8E48A3B/\"\n },\n {\n Name = \"III - Threads of Fate\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448393/67F3BE7DC1BEC807A17D3F8914328D408856E019/\"\n },\n {\n Name = \"IV - The Boundary Beyond 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448568/3C268415A47430CEEC3F9BFB216EF57E3DBF820A/\"\n },\n {\n Name = \"IV - The Boundary Beyond 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448701/3FA23DCA30505AD4C27D8914740AD3138C01122C/\"\n },\n {\n Name = \"IV - The Boundary Beyond 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725448863/266C3320AC326C30F08FBC11A95567E1249E7FB7/\"\n },\n {\n Name = \"V - Heart of the Elders 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449019/A6CC459ACA004C33DD9605BE8382437F6BBE24F7/\"\n },\n {\n Name = \"V - Heart of the Elders 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449150/241FA88572B6B4F652D7FC6D1EDB23DF94E3199C/\"\n },\n {\n Name = \"V - Heart of the Elders 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449310/DBD731498FAB399A4155F7B64F8710BF5FBB5C1F/\"\n },\n {\n Name = \"VI - The City of Archives 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449473/527CB6470F508D4E1F1E4AFC8A0D3AB0A65E046E/\"\n },\n {\n Name = \"VI - The City of Archives 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449630/A8E933F79A646990155CC18A2143B3BC16222750/\"\n },\n {\n Name = \"VI - The City of Archives 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449814/30BB7E4F8D9F1571A420372E78ACAF2D7792811F/\"\n },\n {\n Name = \"VII - The Depths of Yoth 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725449962/D69532827DFF2D654F8A606B7917D593F52D7624/\"\n },\n {\n Name = \"VII - The Depths of Yoth 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450085/479A8A1BDD7BE80B43AE4F2C14DFA811D7B28482/\"\n },\n {\n Name = \"VII - The Depths of Yoth 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450258/E617F5A78DFBE913652E20BF97D38B91087FACAE/\"\n },\n {\n Name = \"VIII - Shattered Aeons 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450415/5631152D4830C00DB8D8EB2163CE773542A62C79/\"\n },\n {\n Name = \"VIII - Shattered Aeons 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725450556/C3000D020B0C32B4DA3420AEE5E286893453FC49/\"\n }\n },\n [\"The Circle Undone\"] = {\n {\n Name = \"0 - Disappearance at the Twilight Estate\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725457506/6E228F87CEDCD3A0CFA28B680C266B4C68C7682B/\"\n },\n {\n Name = \"I - The Witching Hour\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725457706/D0FDC0E9287C343745AE6135352194D654D98B64/\"\n },\n {\n Name = \"II - At Death's Doorstep 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725457864/3106851EC93B1FC01311BD68F02145BA2FD720B0/\"\n },\n {\n Name = \"II - At Death's Doorstep 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458037/155EB572CF28F09A64A021CAC3A3219C31B2CD49/\"\n },\n {\n Name = \"II - At Death's Doorstep 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458187/482CC6730F267061A055D6FD402603BC642A9635/\"\n },\n {\n Name = \"III - The Secret Name 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458373/769D67103FACFB94E77465E85DBB2A250284B59B/\"\n },\n {\n Name = \"III - The Secret Name 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458592/D70CE9B1AD7158AF206A25777A8BA6F284587A83/\"\n },\n {\n Name = \"III - The Secret Name 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725458848/2FC0D5AFFA97AB5E80244F1ED711036B2149B0EE/\"\n },\n {\n Name = \"III - The Secret Name 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459026/A1A5B18ECB38985A35781CBDFF53935541720AF4/\"\n },\n {\n Name = \"IV - The Wages of Sin 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459182/46870E1DBE623E93E675CB2D3C1E959B40CDCC83/\"\n },\n {\n Name = \"IV - The Wages of Sin 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459344/9552699A8CEC5E7BF22843990394BB977539CBE0/\"\n },\n {\n Name = \"IV - The Wages of Sin 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459466/9B6B55A6F76668929B6D6B1DD5FDAE7CEE427A1C/\"\n },\n {\n Name = \"IV - The Wages of Sin 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459609/054A60BDCD266C1A53F29067CFC83D0561666FE4/\"\n },\n {\n Name = \"IV - Wages of Sin 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459714/11B3D734AC5229A0DEA5372CD50F5C7841F9D5A0/\"\n },\n {\n Name = \"V - For the Greater Good 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459829/9AA9563CCD72A8593BDE3C6D1299E96325ECE747/\"\n },\n {\n Name = \"V - For the Greater Good 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725459933/13A103B7BC0ABB611F6A8E61D033C6AD95EED4D4/\"\n },\n {\n Name = \"V - For the Greater Good 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460088/C88DDC5CE898ACEF360B8AD72E05F15BF84DE171/\"\n },\n {\n Name = \"V - For the Greater Good 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460182/C086EDA78636624FC9C4EA1DBCD8F1D39B7A6A89/\"\n },\n {\n Name = \"VI - Union and Disillusion\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460382/8EFB842A623A2D1E6D0E915268A207A5D7DFC6D4/\"\n },\n {\n Name = \"VII - In the Clutches of Chaos 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460586/7E156BF93F211BC425CD37ED273FCC84FF1F4C4D/\"\n },\n {\n Name = \"VII - In the Clutches of Chaos 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460740/72A39A0FACA806EF2E0A4E31AEDF6D8FCDF1A876/\"\n },\n {\n Name = \"VII - In the Clutches of Chaos 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725460884/5716F6E2CB3F8D14FA5B7B806555896088696057/\"\n },\n {\n Name = \"VIII - Before the Black Throne 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725461042/DFF7A992A9440E58CE32D4B8A96A02A8F785AC90/\"\n },\n {\n Name = \"VIII - Before the Black Throne 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725461161/18902B2F74C9D70F5EA79A34521E3FDEDEFC893D/\"\n }\n },\n [\"The Dream-Eaters\"] = {\n {\n Name = \"I-A - Beyond the Gates of Sleep 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725465733/828EE1FF16928EA0426BC68AAED4B9AA6B6FBB49/\"\n },\n {\n Name = \"I-A - Beyond the Gates of Sleep 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725465934/35A85E9056B7A212AC39E7043FDF546A425E62F2/\"\n },\n {\n Name = \"I-A - Beyond the Gates of Sleep 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466105/05D29313D2559B6683465A7034DE7B07DE675420/\"\n },\n {\n Name = \"I-B - Waking Nightmare\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466267/16FAE20612ED0A6A65836DD1B014AC5A801A172F/\"\n },\n {\n Name = \"II-A - The Search for Kadath 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466425/03CCD5F24999CC6CC905CB2FD021DB67D7769163/\"\n },\n {\n Name = \"II-A - The Search for Kadath 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466615/085ACE95FEF03D2BE91BAD5E1864E1D0263DCCC0/\"\n },\n {\n Name = \"II-A - The Search for Kadath 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466802/83861733568FCA9673B6F5EFE0C9D3337E3DFD27/\"\n },\n {\n Name = \"II-A - The Search for Kadath 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725466941/D4FDE1FE722DC6B03AD38D797F2E00531908583D/\"\n },\n {\n Name = \"II-A - The Search for Kadath 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467396/C9A476395B9326142BBDBD06D22A14C83EAD2161/\"\n },\n {\n Name = \"II-B - A Thousand Shapes of Horror 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467582/5EB69F3A3F20B312FE3FE3C64FEA96200EE51563/\"\n },\n {\n Name = \"II-B - A Thousand Shapes of Horror 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467791/5FE437C821641EDD703D336205A37F8F0CACFD38/\"\n },\n {\n Name = \"II-B - A Thousand Shapes of Horror 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725467946/841CEC1D8B56C1CA52B5558E8E49CE04D95D2F4A/\"\n },\n {\n Name = \"III-A - Dark Side of the Moon 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468100/2DD1038163AD5DAA759EA4BE49DB82A2718E931F/\"\n },\n {\n Name = \"III-A - Dark Side of the Moon 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468293/E8145D05833864EEC70AF9AC401039D8E8AFDE3E/\"\n },\n {\n Name = \"III-B - Point of No Return 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468506/1F2E3D425A4D97ADBCA59B4A9401C411F91F875F/\"\n },\n {\n Name = \"III-B - Point of No Return 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468694/8631E68BA7CE69BECF1D476A03CBDC79112A60BF/\"\n },\n {\n Name = \"IV-A - Where the Gods Dwell\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725468880/2C8A9459149D33E526FDC4B68A063475305C15F8/\"\n },\n {\n Name = \"IV-B - Weaver of the Cosmos 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725469060/9E6AF9E0D68EC0F44B82968CE99E433A25A0E0C4/\"\n },\n {\n Name = \"IV-B - Weaver of the Cosmos 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725469224/8C969B8ABAEE6CFFEF165C2E4BBA0E7E9B33AA1A/\"\n },\n {\n Name = \"IV-B - Weaver of the Cosmos 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725469417/A94C14A64836FB9B5FAFAC5B790C51441F8D475F/\"\n }\n },\n [\"The Innsmouth Conspiracy\"] = {\n {\n Name = \"I - The Pit of Despair 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473070/57FEAA8135F62DBAD52E2AAB562EF45EB9A0194A/\"\n },\n {\n Name = \"I - The Pit of Despair 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473257/FEC6F9FAA1CC656BFD5861F58E4751BC94AB0424/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473424/B0E2BFA9C7F61A5B7A72CD11AD1AFF4A070AFFA1/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473570/D28C2DA099D656116A0D7BAA8B874E4ED6B6B50E/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473731/EE9BED3522A1B7CD5E394A67AD7F692DB709A9B5/\"\n },\n {\n Name = \"II - The Vanishing of Elina Harper 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725473874/0A85B2CE9A3FFB048C932E1B014B2986D5D42A1B/\"\n },\n {\n Name = \"II - Vanishing of Elina Harper 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474056/C82E467216DF747AA954C614D7D0B8F649163EDA/\"\n },\n {\n Name = \"III - In Too Deep 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474234/C6A72CDEFCDF1F2A3C6036F349B5F368EFDACA2E/\"\n },\n {\n Name = \"III - In Too Deep 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474398/752D0CE313C58A5B1C3181503C841EB91D041492/\"\n },\n {\n Name = \"III - In Too Deep 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474562/308687DFB9B0F2286A9248698FD38C0BF66B89ED/\"\n },\n {\n Name = \"IV - Devil Reef 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474717/DCEC44D2F7F22A950B3CF52C81C8F931710EFADC/\"\n },\n {\n Name = \"IV - Devil Reef 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725474883/04913E71194BF766DFF2DF8646251AC461A35FE2/\"\n },\n {\n Name = \"V - Horror in High Gear 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475029/FAA34C92FA800E8DDF6449B1C48FCC7468D783BD/\"\n },\n {\n Name = \"V - Horror in High Gear 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475133/BC689B58040F74E7DB44CFD8F3F2BF32FE4081E1/\"\n },\n {\n Name = \"V - Horror in High Gear 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475307/53ADF5E957F88F1DDCE46828547E5808C3CAEFD7/\"\n },\n {\n Name = \"V - Horror in High Gear 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475402/C41EB1D2F8549669DD4323A339F5EC594996816C/\"\n },\n {\n Name = \"VI - A Light in the Fog 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475509/C526C9A3AC34195ACF6161D4821D1424A8A288B4/\"\n },\n {\n Name = \"VI - A Light in the Fog 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725475660/2C67D57967F300BD1FE1AFC08C49EBCF4A88D11C/\"\n },\n {\n Name = \"VII - The Lair of Dagon 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476055/4BC221648012F7A7A37B2F65929705E973FF9CE3/\"\n },\n {\n Name = \"VII - The Lair of Dagon 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476271/3F9CF0FE8ACCE12E26ACA5D5777177A10B49D6DA/\"\n },\n {\n Name = \"VIII - Into the Maelstrom 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476474/AF8695342FBCE6F6BC7B353C86C6006F1629F43A/\"\n },\n {\n Name = \"VIII - Into the Maelstrom 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725476643/CB175401ECDF04F97334CBF8AE7EC76A43A85735/\"\n }\n },\n [\"Edge of the Earth\"] = {\n {\n Name = \"I - Ice and Death 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482152/322E07021DC78455859C860D8B1574F1BA2E0F68/\"\n },\n {\n Name = \"I - Ice and Death 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482327/BD14DDE856A2062A939F860743FFF15F8B0BFF5A/\"\n },\n {\n Name = \"I - Ice and Death 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482520/0FB5AC83DF448994EC0A866ACF6AF57ADCC59C82/\"\n },\n {\n Name = \"??? - Fatal Mirage\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725481929/A35EDF11BD6AF52784BA6611C363CBBB373622EE/\"\n },\n {\n Name = \"II - To the Forbidden Peaks 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482688/B50455616DFC14FE0B9398DBE2A3A1AE25040516/\"\n },\n {\n Name = \"II - To the Forbidden Peaks 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725482839/17786225AA56E75E558491E7E710F555AF3E5799/\"\n },\n {\n Name = \"III - City of the Elder Things 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483014/B74378E02A1A99F544CD98141EF62193A2A612FB/\"\n },\n {\n Name = \"III - City of the Elder Things 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483208/CDFF7A2D404485DE2B4C4ED54B34E197515F0094/\"\n },\n {\n Name = \"IV - The Heart of Madness 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483349/A96BC4AA73C71FE86EF38110236BAEF710C00EE2/\"\n },\n {\n Name = \"IV - The Heart of Madness 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725483528/5BF0DD740C5D43EFBFEF0436EB35FF9DA748FA5E/\"\n }\n },\n [\"The Scarlet Keys\"] = {\n {\n Name = \"5-A Riddles and Rain\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792057358580/E9E5FE4028C08B3D4883406821221B73C8B5B2C7/\"\n },\n {\n Name = \"11-B Dead Heat\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566443853/CAD7771D90141EA6D5FFAFE1EC5E7AD9647C82DB/\"\n },\n {\n Name = \"16-D Sanguine Shadows\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792057358704/4A7261EB31511467CBC46E876476DD205F528A4B/\"\n },\n {\n Name = \"21-F Dealings in the Dark\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792057358816/7C9FE4C34CD0A7AE87EF054742D878F310C71AA7/\"\n },\n {\n Name = \"28-I Dancing Mad\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2037357792056955518/EAB857DD5629EC6A3078FB0A3A703B85B5F514B9/\"\n },\n {\n Name = \"23-K On Thin Ice\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444026/EB5628E254AE25DA89A9C999EAAD995ECF67068E/\"\n },\n {\n Name = \"38-N Dogs of War\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444199/194FD9A713907197471A55411AE300B62C5F5278/\"\n },\n {\n Name = \"46-Q Shades of Suffering\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444330/3ED2CCE95DE933546E1B5CBBF445D773E6D65465/\"\n },\n {\n Name = \"56-Y Without a Trace\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444450/FE4C335B0F72E83900A4EED0FD1A1D304D70D6B7/\"\n },\n {\n Name = \"59-Z Congress of the Keys 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444576/5BB32469ED412D59BB0A46E57D226500B1D0568B/\"\n },\n {\n Name = \"59-Z Congress of the Keys 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2038485431566444690/B01A1FEAB57473D9B6DF11B92D62C214AA1C2C02/\"\n }\n }\n },\n [\"Official Scenarios\"] = {\n [\"The Blob That Ate Everything\"] = {\n {\n Name = \"The Blob That Ate Everything 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489144/53A920E2D1A9F41937B21B0A5B1A4E450ABFC460/\"\n },\n {\n Name = \"The Blob That Ate Everything 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725488396/9CC07F923BD1CBFC4BB06DD2CC6747D2C3541737/\"\n }\n },\n [\"Carnevale of Horrors\"] = {\n {\n Name = \"Carnevale of Horrors 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725496678/91164AF7C2ED06A3E50225794DE9C5E92D1D3B04/\"\n },\n },\n [\"Curse of the Rougarou\"] = {\n {\n Name = \"Curse of the Rougarou 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725494513/DF8E92EA5C1131A0C5D8FA06BFF6FD239412E11C/\"\n },\n {\n Name = \"Curse of the Rougarou 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725494706/5D2EDE5D07DDA6E5781DB70D13F9A17EF26D36F2/\"\n },\n {\n Name = \"Curse of the Rougarou 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725494855/44A751CB66CBE4D75E67A53FC392696F6A3BFD4C/\"\n },\n {\n Name = \"Curse of the Rougarou 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725495011/ADD3431B383236BCF057C1573CEF7E43F39FDE72/\"\n },\n {\n Name = \"Curse of the Rougarou 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725495146/79159992D5002AD3D7D23ED82D9BB76D437B94C3/\"\n }\n },\n [\"Guardians of the Abyss\"] = {\n {\n Name = \"Guardians of the Abyss 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725490870/9C03B2974D7776B8E2AFEEF025756D1885AF0AE3/\"\n },\n {\n Name = \"Guardians of the Abyss 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491039/E81F05AB9C802F57F4D9F7229CE3D53231ACB70D/\"\n },\n {\n Name = \"Guardians of the Abyss 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491202/384AA7DBA614B61C46D57EF5105A96F25F938B74/\"\n },\n {\n Name = \"Guardians of the Abyss 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491392/1C8FF752DF97F362BA8937BFEC65140AE3ADF8A6/\"\n },\n {\n Name = \"Guardians of the Abyss 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491512/06B1DCA10B44FF86567CEFCC135D619648CE5F19/\"\n },\n {\n Name = \"Guardians of the Abyss 6\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725491658/5446E231AEF72CF4F7B4892116671FF7175EFA0F/\"\n }\n },\n [\"The Labyrinths of Lunacy\"] = {\n {\n Name = \"The Labyrinths of Lunacy 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489685/D2D342844212C8A21E030418935A227C2E3279DB/\"\n },\n {\n Name = \"The Labyrinths of Lunacy 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489820/E3E18B0940C2604F62E564AD43F178FF9F13B3C9/\"\n },\n {\n Name = \"The Labyrinths of Lunacy 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489972/6A34CF53190EAAAF57C31FB97A3C2ACBD27FEE40/\"\n }\n },\n [\"Murder at the Excelsior Hotel\"] = {\n {\n Name = \"Murder at the Excelsior Hotel 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725488868/7F7FE8BB3C7E3645B4377F86366C6073CDB8F113/\"\n },\n {\n Name = \"Murder at the Excelsior Hotel 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725489144/53A920E2D1A9F41937B21B0A5B1A4E450ABFC460/\"\n }\n },\n [\"War of the Outer Gods\"] = {\n {\n Name = \"War of the Outer Gods\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725487627/43A19A6D97A6DA63A487EB247EE95884E2D9F5FD/\"\n }\n }\n },\n [\"Fan-Made Campaigns\"] = {\n [\"Cyclopean Foundations\"] = {\n {\n Name = \"I - Lost Moorings 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725499518/02DA17FC9D6A1174E484977269F44AE6995C6F7C/\"\n },\n {\n Name = \"I - Lost Moorings 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725499680/1556F230B59D42FCA47A5A87135330B03C231E92/\"\n },\n {\n Name = \"II - Going Twice\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725499855/1220C81273EFA6F36C2AEBA713E31DE1E2F92454/\"\n },\n {\n Name = \"III - Private Lives\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500069/452E4427185A7F1AFB3F2CBB263DC55B6A144D49/\"\n },\n {\n Name = \"IV - Crumbling Masonry 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500260/94FE8506AB13CA51F087ABF155799F73BCBAB1E9/\"\n },\n {\n Name = \"IV - Crumbling Masonry 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500508/04B6C6D8845BF5411D28C79D185EAF6FFBB409F5/\"\n },\n {\n Name = \"V - Across Dreadful Waters\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500640/447069BBBF70474439D3CC4F970F6617C8B738F4/\"\n },\n {\n Name = \"VI - Blood From Stones\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500803/23C46C5F08E3F911F565D9EC38CFACFFAA5F5B11/\"\n },\n {\n Name = \"VII - Pyroclastic Flow 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725500952/804B531E517B1AD2294E304AA6F72F8CC3E6FC4E/\"\n },\n {\n Name = \"VII - Pyroclastic Flow 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501115/E771289EB848A695DF47C405300B1EC7CA925009/\"\n },\n {\n Name = \"VIII - Tomb of Dead Dreams 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501272/CAE6C4185540F3F8FFA2A8E37458A5B80DBD70FC/\"\n },\n {\n Name = \"VIII - Tomb of Dead Dreams 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501415/DA9C07488DA2753FF5621D1731A84D6D0B1CC1BD/\"\n },\n {\n Name = \"VIII - Tomb of Dead Dreams 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725501813/030E5DB9F8F6C5F092EFDF2C42AD631318B0923A/\"\n }\n },\n [\"Dark Matter\"] = {\n {\n Name = \"I - The Tatterdemalion 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504118/AC852F478D5BDA0C8A54A499B07A66E872560EC7/\"\n },\n {\n Name = \"I - The Tatterdemalion 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504319/5B24BB2080AC76D836708AABC1BC90FD884F043D/\"\n },\n {\n Name = \"I - The Tatterdemalion 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504461/73E4632A2EAAFA918924E60A64B03838CA6DDD77/\"\n },\n {\n Name = \"I - The Tatterdemalion 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504602/6DB8F081CC907A0D6F364E5045BB7E8FADA91B5C/\"\n },\n {\n Name = \"II - Electric Nightmares 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504770/C6FB5FDD153ACD07565259AE013FDC7FF567037D/\"\n },\n {\n Name = \"II - Electric Nightmares 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725504974/80E29EA68B88B78CDADC475998E832C7245409F4/\"\n },\n {\n Name = \"IIIa - Lost Quantum\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725505211/07E1254B1C601496E47B7E60B736D1699AAD38C8/\"\n },\n {\n Name = \"IIIb - In the Shadow of Earth 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725505419/7E8B50470AD6AD27429B58A42271755289FC90EB/\"\n },\n {\n Name = \"IIIb - In the Shadow of Earth 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725505603/DCD5B255FE66F8D8F47FB6C928D80583F3F950AC/\"\n },\n {\n Name = \"IIIc - Strange Moons\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506071/78597C2FBE2DE55BFC86AEC9F42FE1B20D26544C/\"\n },\n {\n Name = \"IV - The Machine in Yellow\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725426327/41F6192EDCFFD6AAE2EE44C2BB5708B19D7464A9/\"\n },\n {\n Name = \"V - Fragment of Carcosa 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506388/A299C8D58171A6D78CF55D911C2B81C63D88444F/\"\n },\n {\n Name = \"V - Fragment of Carcosa 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506567/C36509CB6520803355E7AE1E9EC8CD35641B28C8/\"\n },\n {\n Name = \"VI - Starfall 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506714/327D0E565039B7FA9157FDA04A302DF178C64C44/\"\n },\n {\n Name = \"VI - Starfall 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506809/E637DAAD8BEB00E4305756D101E1E6B370CB1644/\"\n },\n {\n Name = \"VI - Starfall 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725506978/A17CD45A7770BAC7196218ADD2FB6CEB0E7A0E6B/\"\n }\n },\n [\"The Ghosts of Onigawa\"] = {\n {\n Name = \"I - The Hidden Village\",\n URL = \"https://i.imgur.com/btTQffc.jpeg\"\n },\n {\n Name = \"II - The House on the Hill\",\n URL = \"https://i.imgur.com/YTHt8JQ.jpeg\"\n },\n {\n Name = \"III - The River Delta\",\n URL = \"https://i.imgur.com/9Zk3iLJ.png\"\n },\n {\n Name = \"IV - The War Eternal\",\n URL = \"https://i.imgur.com/6UtFHhc.jpeg\"\n },\n {\n Name = \"V - Half Light\",\n URL = \"https://i.imgur.com/hul7lLL.png\"\n },\n {\n Name = \"VI - The End of August\",\n URL = \"https://i.imgur.com/PKWtpG7.jpeg\"\n },\n {\n Name = \"VII - The Molten Armory\",\n URL = \"https://i.imgur.com/kMSdBRh.jpeg\"\n },\n {\n Name = \"VIII - The Black Harvest\",\n URL = \"https://i.imgur.com/6ySucTS.jpeg\"\n }\n }\n },\n [\"Fan-Made Scenarios\"] = {\n [\"Side Scenarios (FM)\"] = {\n {\n Name = \"Code Red at Bleeding Heart\",\n URL = \"https://i.imgur.com/nTstdlj.jpeg\"\n },\n {\n Name = \"Consternation on the Constellation\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725512402/37F34A14CEEA9D2F889F7B97B065C0193F268FE1/\"\n },\n {\n Name = \"Symphony of Erich Zann\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725512613/B23EA91E9489E0DDE250DD33F9AF1A12EEE52E0C/\"\n }\n }\n },\n [\"Other Images\"] = {\n [\"Arkham Locations\"] = {\n {\n Name = \"Downtown 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516086/53F48F8AA9CFE4BF544BF03A616AC12A5344615C/\"\n },\n {\n Name = \"Downtown 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516316/1A337F5D66D7B5F59C66F465710717340B1D56AB/\"\n },\n {\n Name = \"Eastside 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516542/09B4BE1E5487C3B11C0178B0B6FFD51620BE6AA6/\"\n },\n {\n Name = \"Eastside 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516744/9B7F54E99D9B85884D5E363B97B25DA2DD3F03CC/\"\n },\n {\n Name = \"French Hill\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725516949/8BFEF09FDB6608173280F887C1BA3906427678CC/\"\n },\n {\n Name = \"Generic 1\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517086/93E00AA823FBA1C6531BBA143408E1E7D89BE3F0/\"\n },\n {\n Name = \"Generic 2\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517270/1450BCB6698058F833FAE770D31D0D064B82F5C2/\"\n },\n {\n Name = \"Generic 3\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517434/5B529D9DEF3550E898744CE19AAD4CC0AB3F12DF/\"\n },\n {\n Name = \"Generic 4\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517622/FE0C331881B776E66D3A2C60D70147A0CABFDAC6/\"\n },\n {\n Name = \"Generic 5\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725517833/A123F1A2D1B8BE960AAB371D9FD06F27C282CB9B/\"\n },\n {\n Name = \"Generic 6\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518026/10D4F06C8A1AEC3ACAEC5C3B9DA142D3BA5818FE/\"\n },\n {\n Name = \"Generic 7\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518144/72D4A60264C2DD201AAA5FFDCA95C6FF04EF8AC8/\"\n },\n {\n Name = \"Generic 8\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518292/A1848A68389FBECA5BC847F90A921C18133B17D6/\"\n },\n {\n Name = \"Merchant District\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518454/BD6694DCCD12E31997202B2020B4A29FDC96FC7B/\"\n },\n {\n Name = \"Miskatonic University\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725518846/79D6BEB0D8C274E3D308A67CD3181418B02D3A7E/\"\n },\n {\n Name = \"Northside\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519012/E4B527768AFEFE0E4FB4518CA2AFDD69A98AB5D1/\"\n },\n {\n Name = \"Rivertown\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519190/7A9A09861EECE2D98B5C1EB694E88C2073ABDFDF/\"\n },\n {\n Name = \"Southside\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519353/FE43349A25231F48662309FEB331DB2C418A5E80/\"\n },\n {\n Name = \"Uptown\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725519551/3630457608AD2DB605EC4BCF4332C1D0983305C0/\"\n }\n },\n [\"Default Image\"] = {\n {\n Name = \"Default Image\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/998015670465071049/FFAE162920D67CF38045EFBD3B85AD0F916147B2/\"\n }\n },\n [\"Unsorted\"] = {\n {\n Name = \"Kingsport\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2279446315725522594/1EA2C0AF5D4D346AD3FFDC38215BB20AAA72CE8D/\"\n },\n {\n Name = \"Devil\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2115062479282248687/DD84A3CB3C4A475A5D093CB413A16A5CEA5FBF79/\"\n },\n {\n Name = \"Mystic Board\",\n URL = \"http://cloud-3.steamusercontent.com/ugc/2115062479282248488/EC27B1215F558A39954C27477D8B4F916CA211E5/\"\n }\n }\n }\n}\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"selectionIndex\":1,\"typeIndex\":1}", "MeasureMovement": false, "Name": "Custom_Token", - "Nickname": "Playmat Image Swapper", + "Nickname": "PlayArea Image Swapper", "Snap": true, "Sticky": true, "Tags": [ @@ -88664,7 +32382,7 @@ }, "Description": "The Anomaly", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"89001\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Manifold.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"cycle\": \"Standalone\",\n \"deck_requirements\": {\n \"size\": 50,\n \"randomBasicWeaknessCount\": 2,\n \"signatures\": [\n {\n \"89002\": 1\n },\n {\n \"89003\": 3\n },\n {\n \"89004\": 3\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"permanent\": true,\n \"level\": {\n \"min\": 0,\n \"max\": 5\n },\n \"error\": \"No permanents except story and signature permanents\"\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"seeker\",\n \"rogue\",\n \"mystic\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"error\": \"You must have at least 7 cards from each class\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"89001\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Manifold.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"cycle\": \"The Blob That Ate Everything ELSE\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 50,\n \"randomBasicWeaknessCount\": 2,\n \"signatures\": [\n {\n \"89002\": 1\n },\n {\n \"89003\": 3\n },\n {\n \"89004\": 3\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"permanent\": true,\n \"level\": {\n \"min\": 0,\n \"max\": 5\n },\n \"error\": \"No permanents except story and signature permanents\"\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"seeker\",\n \"rogue\",\n \"mystic\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"error\": \"You must have at least 7 cards from each class\"\n }\n ]\n}", "GUID": "758b0a", "Grid": true, "GridProjection": false, @@ -88787,7 +32505,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"89003\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 0,\n \"traits\": \"Power.\",\n \"wildIcons\": 1,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"89003\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 0,\n \"traits\": \"Power.\",\n \"wildIcons\": 1,\n \"cycle\": \"The Blob That Ate Everything ELSE\"\n}", "GUID": "0a1b3a", "Grid": true, "GridProjection": false, @@ -88848,7 +32566,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"89004\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Power.\",\n \"weakness\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"89004\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Power.\",\n \"weakness\": true,\n \"cycle\": \"The Blob That Ate Everything ELSE\"\n}", "GUID": "0a1b3a", "Grid": true, "GridProjection": false, @@ -88909,7 +32627,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"89005\",\n \"type\": \"Story\",\n \"class\": \"Neutral\",\n \"permanent\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"89005\",\n \"type\": \"Story\",\n \"class\": \"Neutral\",\n \"permanent\": true,\n \"cycle\": \"The Blob That Ate Everything ELSE\"\n}", "GUID": "858b0a", "Grid": true, "GridProjection": false, @@ -88970,7 +32688,7 @@ }, "Description": "(Un)-Controlled Hunger", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"89002\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"traits\": \"Talent.\",\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"89002\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"traits\": \"Talent.\",\n \"cycle\": \"The Blob That Ate Everything ELSE\"\n}", "GUID": "558b0a", "Grid": true, "GridProjection": false, @@ -89032,7 +32750,7 @@ }, "Description": "The Reporter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02002-p\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Hunting for Answers\"\n}", + "GMNotes": "{\n \"id\": \"02002-p\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Hunting for Answers\",\n \"extraToken\": \"None\"\n}", "GUID": "0a5492", "Grid": true, "GridProjection": false, @@ -89094,7 +32812,7 @@ }, "Description": "The Reporter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02002-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Hunting for Answers\"\n}", + "GMNotes": "{\n \"id\": \"02002-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Hunting for Answers\",\n \"extraToken\": \"Reaction\"\n}", "GUID": "0a5493", "Grid": true, "GridProjection": false, @@ -89156,7 +32874,7 @@ }, "Description": "The Reporter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02002-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Hunting for Answers\"\n}", + "GMNotes": "{\n \"id\": \"02002-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Hunting for Answers\",\n \"extraToken\": \"None\"\n}", "GUID": "0a5494", "Grid": true, "GridProjection": false, @@ -89401,7 +33119,7 @@ }, "Description": "The Bootlegger", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"04003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"The Forgotten Age\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04010\": 1\n },\n {\n \"04011\": 1\n },\n {\n \"04012\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"illicit\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Seeker and/or Survivor cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"04003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"The Forgotten Age\",\n \"extraToken\": \"Evade\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04010\": 1\n },\n {\n \"04011\": 1\n },\n {\n \"04012\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"illicit\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Seeker and/or Survivor cards\"\n }\n ]\n}", "GUID": "dd40c0", "Grid": true, "GridProjection": false, @@ -89463,7 +33181,7 @@ }, "Description": "The Archeologist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"08007\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Edge of the Earth\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"08007\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Edge of the Earth\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", "GUID": "46b145", "Grid": true, "GridProjection": false, @@ -89525,7 +33243,7 @@ }, "Description": "The Archeologist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"08007-p\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Relics of the Past\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"relic\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"08007-p\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Relics of the Past\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"relic\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", "GUID": "46b146", "Grid": true, "GridProjection": false, @@ -89587,7 +33305,7 @@ }, "Description": "The Archeologist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"08007-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Relics of the Past\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"relic\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"08007-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Relics of the Past\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"relic\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", "GUID": "46b148", "Grid": true, "GridProjection": false, @@ -89649,7 +33367,7 @@ }, "Description": "The Archeologist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"08007-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Relics of the Past\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"08007-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"Relics of the Past\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08008\": 1,\n \"90063\": 1\n },\n {\n \"08009\": 1,\n \"90064\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Seeker cards\"\n }\n ]\n}", "GUID": "46b147", "Grid": true, "GridProjection": false, @@ -89860,7 +33578,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/BookofLivingMyths\")\nend)\n__bundle_register(\"playercards/cards/BookofLivingMyths\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad(savedData)\n self.addContextMenuItem(\"Enable Helper\", createButtons)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.loopId then\n createButtons()\n end\n end\nend\n\nfunction deleteButtons()\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", createButtons)\n self.UI.setAttribute(\"inactives\", \"active\", false)\n self.UI.setAttribute(\"actives\", \"active\", false)\n if loopId then Wait.stop(loopId) end\n loopId = nil\n self.script_state = JSON.encode({ loopId = loopId })\nend\n\n-- create buttons and begin monitoring chaos bag for curse and bless tokens\nfunction createButtons()\n self.clearContextMenu()\n self.addContextMenuItem(\"Clear Helper\", deleteButtons)\n self.UI.setAttribute(\"inactives\", \"active\", true)\n self.UI.setAttribute(\"actives\", \"active\", true)\n loopId = Wait.time(maybeUpdateButtonState, 1, -1)\n self.script_state = JSON.encode({ loopId = loopId })\nend\n\nfunction resolveToken(player, _, tokenType)\n local matColor\n if player.color == \"Black\" then\n matColor = playmatApi.getMatColorByPosition(self.getPosition())\n else\n matColor = playmatApi.getMatColor(player.color)\n end\n\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n chaosBagApi.drawChaosToken(mat, true, tokenType)\nend\n\n-- count tokens in the bag and show appropriate buttons\nfunction maybeUpdateButtonState()\n local numInBag = getBlessCurseInBag()\n local state = { Bless = false, Curse = false }\n\n if numInBag.Bless \u003e= numInBag.Curse and numInBag.Bless \u003e 0 then\n state.Bless = true\n end\n\n if numInBag.Curse \u003e= numInBag.Bless and numInBag.Curse \u003e 0 then\n state.Curse = true\n end\n\n setUiState(state)\nend\n\nfunction getBlessCurseInBag()\n local numInBag = { Bless = 0, Curse = 0 }\n local chaosBag = chaosBagApi.findChaosBag()\n\n for _, v in ipairs(chaosBag.getObjects()) do\n if v.name == \"Bless\" then\n numInBag.Bless = numInBag.Bless + 1\n elseif v.name == \"Curse\" then\n numInBag.Curse = numInBag.Curse + 1\n end\n end\n\n return numInBag\nend\n\nfunction setUiState(params)\n -- set bless state\n if params.Bless then\n self.UI.show(\"Bless\")\n self.UI.hide(\"inactiveBless\")\n else\n self.UI.show(\"inactiveBless\")\n self.UI.hide(\"Bless\")\n end\n\n -- set curse state\n if params.Curse then\n self.UI.show(\"Curse\")\n self.UI.hide(\"inactiveCurse\")\n else\n self.UI.show(\"inactiveCurse\")\n self.UI.hide(\"Curse\")\n end\nend\n\nfunction errorMessage()\n local numInBag = getBlessCurseInBag()\n\n if numInBag.Bless == 0 and numInBag.Curse == 0 then\n broadcastToAll(\"There are no Bless or Curse tokens in the chaos bag.\", \"Red\")\n elseif numInBag.Bless \u003e numInBag.Curse then\n broadcastToAll(\"There are more Bless tokens than Curse tokens in the chaos bag.\", \"Red\")\n else\n broadcastToAll(\"There are more Curse tokens than Bless tokens in the chaos bag.\", \"Red\")\n end\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/BookofLivingMyths\")\nend)\n__bundle_register(\"playercards/cards/BookofLivingMyths\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\nlocal updated, loopId\n\nfunction updateSave()\n self.script_state = JSON.encode({\n isHelperEnabled = isHelperEnabled,\n loopId = loopId\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n loopId = loadedData.loopId\n end\n syncDisplayWithOptionPanel()\nend\n\nfunction shutOff()\n if loopId then\n Wait.stop(loopId)\n loopId = nil\n end\nend\n\nfunction initialize()\n maybeUpdateButtonState()\n loopId = Wait.time(maybeUpdateButtonState, 1, -1)\nend\n\nfunction resolveToken(player, _, tokenType)\n if not updated then return end\n local matColor\n if player.color == \"Black\" then\n matColor = playermatApi.getMatColorByPosition(self.getPosition())\n else\n matColor = playermatApi.getMatColor(player.color)\n end\n\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n chaosBagApi.drawChaosToken(mat, true, tokenType)\n updated = false\n Wait.frames(maybeUpdateButtonState, 2)\nend\n\n-- count tokens in the bag and show appropriate buttons\nfunction maybeUpdateButtonState()\n local numInBag = blessCurseManagerApi.getBlessCurseInBag()\n local state = { Bless = false, Curse = false }\n\n if numInBag.Bless \u003e= numInBag.Curse and numInBag.Bless \u003e 0 then\n state.Bless = true\n end\n\n if numInBag.Curse \u003e= numInBag.Bless and numInBag.Curse \u003e 0 then\n state.Curse = true\n end\n\n setUiState(state)\n updated = true\nend\n\nfunction setUiState(params)\n for _, tokenName in ipairs({ \"Bless\", \"Curse\" }) do\n if params[tokenName] then\n self.UI.show(tokenName)\n self.UI.hide(\"inactive\" .. tokenName)\n else\n self.UI.show(\"inactive\" .. tokenName)\n self.UI.hide(tokenName)\n end\n end\nend\n\nfunction errorMessage()\n local numInBag = blessCurseManagerApi.getBlessCurseInBag()\n\n if numInBag.Bless == 0 and numInBag.Curse == 0 then\n broadcastToAll(\"There are no Bless or Curse tokens in the chaos bag.\", \"Red\")\n elseif numInBag.Bless \u003e numInBag.Curse then\n broadcastToAll(\"There are more Bless tokens than Curse tokens in the chaos bag.\", \"Red\")\n else\n broadcastToAll(\"There are more Curse tokens than Bless tokens in the chaos bag.\", \"Red\")\n end\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -89885,7 +33603,7 @@ "scaleZ": 1 }, "Value": 0, - "XmlUI": "\u003c!-- include playercards/BookofLivingMyths.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton padding=\"50 50 50 50\"\n font=\"font_teutonic-arkham\"\n fontSize=\"300\"\n iconWidth=\"400\"\n iconAlignment=\"Right\"\n text=\"Resolve\"/\u003e\n \u003cButton class=\"inactive\"\n onClick=\"errorMessage\"\n color=\"#353535E6\"\n textColor=\"#A0A0A0\"/\u003e\n \u003cButton class=\"active\"\n onClick=\"resolveToken\"\n textColor=\"white\"\n active=\"false\"/\u003e\n \u003cPanel position=\"0 -55 -22\"\n rotation=\"0 0 180\"\n height=\"900\"\n width=\"1400\"\n scale=\"0.1 0.1 1\"/\u003e\n \u003cTableLayout active=\"false\"\n cellSpacing=\"80\"\n cellBackgroundColor=\"rgba(1,1,1,0)\"/\u003e\n\u003c/Defaults\u003e\n\n\u003cPanel\u003e\n \u003cTableLayout id=\"actives\"\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"Bless\"\n icon=\"bless\"\n color=\"#9D702CE6\"\n class=\"active\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"Curse\"\n icon=\"curse\"\n color=\"#633A84E6\"\n class=\"active\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n\u003c/Panel\u003e\n\n\u003cPanel\u003e\n \u003cTableLayout id=\"inactives\"\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"inactiveBless\"\n icon=\"bless\"\n class=\"inactive\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"inactiveCurse\"\n icon=\"curse\"\n class=\"inactive\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n\u003c/Panel\u003e\n\u003c!-- include playercards/BookofLivingMyths.xml --\u003e" + "XmlUI": "\u003c!-- include playercards/BookofLivingMyths.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton padding=\"50 50 50 50\"\n font=\"font_teutonic-arkham\"\n fontSize=\"300\"\n iconWidth=\"400\"\n iconAlignment=\"Right\"\n text=\"Resolve\"/\u003e\n \u003cButton class=\"inactive\"\n onClick=\"errorMessage\"\n color=\"#353535E6\"\n textColor=\"#A0A0A0\"/\u003e\n \u003cButton class=\"active\"\n onClick=\"resolveToken\"\n textColor=\"white\"\n active=\"false\"/\u003e\n \u003cTableLayout position=\"0 -55 -22\"\n rotation=\"0 0 180\"\n height=\"900\"\n width=\"1400\"\n scale=\"0.1 0.1 1\"\n cellSpacing=\"80\"\n cellBackgroundColor=\"rgba(1,1,1,0)\"/\u003e\n\u003c/Defaults\u003e\n\n\u003cPanel id=\"Helper\"\n active=\"false\"\u003e\n \u003cTableLayout\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"Bless\"\n icon=\"bless\"\n color=\"#9D702CE6\"\n class=\"active\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"Curse\"\n icon=\"curse\"\n color=\"#633A84E6\"\n class=\"active\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003cTableLayout\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"inactiveBless\"\n icon=\"bless\"\n class=\"inactive\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"inactiveCurse\"\n icon=\"curse\"\n class=\"inactive\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n\u003c/Panel\u003e\n\u003c!-- include playercards/BookofLivingMyths.xml --\u003e" }, { "AltLookAngle": { @@ -90035,7 +33753,7 @@ }, "Description": "The Folklorist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"10012\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Scholar. Blessed. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 1,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10013\": 1\n },\n {\n \"10014\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"trait\": [\n \"blessed\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"10012\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Scholar. Blessed. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 1,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10013\": 1\n },\n {\n \"10014\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"trait\": [\n \"blessed\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n }\n ]\n}", "GUID": "54eaa7", "Grid": true, "GridProjection": false, @@ -90044,7 +33762,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/KohakuNarukami\")\nend)\n__bundle_register(\"playercards/cards/KohakuNarukami\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\nlocal updated, loopId\n\nlocal xmlData = {\n Action = { color = \"#6D202CE6\", onClick = \"removeAndExtraAction\" },\n Bless = { color = \"#9D702CE6\", onClick = \"addTokenToBag\" },\n Curse = { color = \"#633A84E6\", onClick = \"addTokenToBag\" },\n ElderSign = { color = \"#50A8CEE6\", onClick = \"elderSignAbility\" }\n}\n\nfunction updateSave()\n self.script_state = JSON.encode({\n isHelperEnabled = isHelperEnabled,\n loopId = loopId\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n loopId = loadedData.loopId\n end\n syncDisplayWithOptionPanel()\nend\n\nfunction initialize()\n maybeUpdateButtonState()\n loopId = Wait.time(maybeUpdateButtonState, 1, -1)\nend\n\nfunction shutOff()\n if loopId then\n Wait.stop(loopId)\n loopId = nil\n end\nend\n\nfunction addTokenToBag(_, _, tokenType)\n if not updated then return end\n blessCurseManagerApi.addToken(tokenType)\n updated = false\n Wait.frames(maybeUpdateButtonState, 2)\nend\n\nfunction removeAndExtraAction()\n if not updated then return end\n blessCurseManagerApi.removeToken(\"Bless\")\n blessCurseManagerApi.removeToken(\"Bless\")\n blessCurseManagerApi.removeToken(\"Curse\")\n blessCurseManagerApi.removeToken(\"Curse\")\n updated = false\n Wait.frames(maybeUpdateButtonState, 2)\n\n local position = self.getPosition()\n local matColor = playermatApi.getMatColorByPosition(position)\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n local rotation = mat.getRotation()\n\n -- find empty action token slots by checking snap points\n local snaps = mat.getSnapPoints()\n\n -- get first empty slot in the top row (so the second empty slot because of the ability token)\n local emptyPos = position -- default to card position\n for i, snap in ipairs(snaps) do\n if i \u003e 1 then\n if snap.tags[1] == \"UniversalToken\" then\n local snapPos = mat.positionToWorld(snap.position)\n local searchResult = searchLib.atPosition(snapPos, \"isUniversalToken\")\n if #searchResult == 0 then\n emptyPos = snapPos\n break\n end\n end\n end\n end\n\n callback = function(spawned)\n spawned.call(\"updateClassAndSymbol\", { class = \"Mystic\", symbol = \"Mystic\" })\n spawned.addTag(\"Temporary\")\n end\n tokenManager.spawnToken(emptyPos + Vector(0, 0.7, 0), \"universalActionAbility\", rotation, callback)\nend\n\nfunction elderSignAbility()\n blessCurseManagerApi.addToken(\"Bless\")\n blessCurseManagerApi.addToken(\"Curse\")\n updated = false\n Wait.frames(maybeUpdateButtonState, 2)\nend\n\n-- count tokens in the bag and show appropriate buttons\nfunction maybeUpdateButtonState()\n local numInBag = blessCurseManagerApi.getBlessCurseInBag()\n local state = { Bless = false, Curse = false, Action = false, ElderSign = false }\n\n if numInBag.Bless \u003c= numInBag.Curse and numInBag.Bless \u003c 10 then\n state.Bless = true\n state.ElderSign = true\n end\n\n if numInBag.Curse \u003c= numInBag.Bless and numInBag.Curse \u003c 10 then\n state.Curse = true\n state.ElderSign = true\n end\n\n if numInBag.Curse \u003e= 2 and numInBag.Bless \u003e= 2 then\n state.Action = true\n end\n\n setUiState(state)\n updated = true\nend\n\nfunction setUiState(params)\n for buttonId, state in pairs(params) do\n if state then\n self.UI.setAttribute(buttonId, \"color\", xmlData[buttonId].color)\n self.UI.setAttribute(buttonId, \"onClick\", xmlData[buttonId].onClick)\n self.UI.setAttribute(buttonId, \"textColor\", \"white\")\n else\n self.UI.setAttribute(buttonId, \"color\", \"#353535E6\")\n self.UI.setAttribute(buttonId, \"onClick\", \"errorMessage\")\n self.UI.setAttribute(buttonId, \"textColor\", \"#A0A0A0\")\n end\n end\nend\n\nfunction errorMessage(_, _, triggeringButton)\n local numInBag = blessCurseManagerApi.getBlessCurseInBag()\n if triggeringButton == \"Action\" then\n broadcastToAll(\"There are not enough Blesses and/or Curses in the chaos bag.\", \"Red\")\n elseif numInBag.Bless == 10 and numInBag.Curse == 10 then\n broadcastToAll(\"No more tokens can be added to the chaos bag.\", \"Red\")\n elseif numInBag.Bless \u003c numInBag.Curse then\n broadcastToAll(\"There are more Bless tokens than Curse tokens in the chaos bag.\", \"Red\")\n else\n broadcastToAll(\"There are more Curse tokens than Bless tokens in the chaos bag.\", \"Red\")\n end\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -90069,7 +33787,7 @@ "scaleZ": 1.15 }, "Value": 0, - "XmlUI": "" + "XmlUI": "\u003c!-- include playercards/KohakuNarukami.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton padding=\"50 50 50 50\"\n font=\"font_teutonic-arkham\"\n fontSize=\"200\"\n iconWidth=\"300\"\n iconAlignment=\"Right\"/\u003e\n \u003cTableLayout position=\"0 188 -22\"\n rotation=\"0 0 90\"\n height=\"1800\"\n width=\"700\"\n scale=\"0.1 0.1 1\"\n cellSpacing=\"80\"\n cellBackgroundColor=\"rgba(1,1,1,0)\"/\u003e\n\u003c/Defaults\u003e\n\n\u003cTableLayout id=\"Helper\"\nactive=\"false\"\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"Bless\"\n icon=\"token-bless\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"Curse\"\n icon=\"token-curse\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"Action\"\n text=\"Remove tokens\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton id=\"ElderSign\"\n icon=\"token-eldersign\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include playercards/KohakuNarukami.xml --\u003e" }, { "AltLookAngle": { @@ -90649,7 +34367,7 @@ }, "Description": "The Scientist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"10004\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic. Scholar.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10005\": 1\n },\n {\n \"10008\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"science\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"insight\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"10004\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic. Scholar.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10005\": 1\n },\n {\n \"10008\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"science\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"insight\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n }\n }\n ]\n}", "GUID": "ce2322", "Grid": true, "GridProjection": false, @@ -91088,7 +34806,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Strong-Armed\")\nend)\n__bundle_register(\"playercards/cards/Strong-Armed\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -91570,7 +35288,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"10066\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Rogue\",\n \"cost\": 3,\n \"level\": 0,\n \"traits\": \"Item. Illicit.\",\n \"intellectIcons\": 1,\n \"cycle\": \"The Feast of Hemlock Vale\"\n}", + "GMNotes": "{\n \"id\": \"10066\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Rogue\",\n \"cost\": 3,\n \"level\": 0,\n \"traits\": \"Item. Illicit.\",\n \"intellectIcons\": 1,\n \"uses\": [\n {\n \"count\": 0,\n \"type\": \"Suspicion\",\n \"token\": \"resource\"\n }\n ],\n \"cycle\": \"The Feast of Hemlock Vale\"\n}", "GUID": "acd38d", "Grid": true, "GridProjection": false, @@ -91694,7 +35412,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90048\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Hardship.\",\n \"weakness\": true,\n \"cycle\": \"The Dunwich Legacy\"\n}", + "GMNotes": "{\n \"id\": \"90048\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Hardship.\",\n \"weakness\": true,\n \"cycle\": \"On the Road Again\"\n}", "GUID": "876557", "Grid": true, "GridProjection": false, @@ -91755,7 +35473,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90047\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Instrument.\",\n \"cycle\": \"The Dunwich Legacy\"\n}", + "GMNotes": "{\n \"id\": \"90047\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Instrument.\",\n \"cycle\": \"On the Road Again\"\n}", "GUID": "876557", "Grid": true, "GridProjection": false, @@ -91817,7 +35535,7 @@ }, "Description": "The Drifter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02005-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Survivor or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02005-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"On the Road Again\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Survivor or Neutral\"\n }\n ]\n}", "GUID": "5294c3", "Grid": true, "GridProjection": false, @@ -91879,7 +35597,7 @@ }, "Description": "The Drifter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02005-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"improvised\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"faction\": [\n \"guardian\"\n ],\n \"error\": \"You cannot have more than 5 level 0 Guardian cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02005-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"On the Road Again\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"improvised\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"faction\": [\n \"guardian\"\n ],\n \"error\": \"You cannot have more than 5 level 0 Guardian cards\"\n }\n ]\n}", "GUID": "5294c3", "Grid": true, "GridProjection": false, @@ -91941,7 +35659,7 @@ }, "Description": "The Drifter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02005-p\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"improvised\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"faction\": [\n \"guardian\"\n ],\n \"error\": \"You cannot have more than 5 level 0 Guardian cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02005-p\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"On the Road Again\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"improvised\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"faction\": [\n \"guardian\"\n ],\n \"error\": \"You cannot have more than 5 level 0 Guardian cards\"\n }\n ]\n}", "GUID": "5294c3", "Grid": true, "GridProjection": false, @@ -92371,7 +36089,7 @@ }, "Description": "John Dee Translation (Advanced)", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90003\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Neutral\",\n \"traits\": \"Item. Tome.\",\n \"uses\": [\n {\n \"count\": 3,\n \"type\": \"Horror\",\n \"token\": \"horror\"\n }\n ],\n \"weakness\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90003\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Neutral\",\n \"traits\": \"Item. Tome.\",\n \"uses\": [\n {\n \"count\": 3,\n \"type\": \"Horror\",\n \"token\": \"horror\"\n }\n ],\n \"weakness\": true,\n \"cycle\": \"Read or Die\"\n}", "GUID": "5b2e10", "Grid": true, "GridProjection": false, @@ -93242,7 +36960,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FluteoftheOuterGods4\")\nend)\n__bundle_register(\"playercards/cards/FluteoftheOuterGods4\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nSHOW_SINGLE_RELEASE = true\nKEEP_OPEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FluteoftheOuterGods4\")\nend)\n__bundle_register(\"playercards/cards/FluteoftheOuterGods4\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nMAX_SEALED = 10\nKEEP_OPEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -93612,7 +37330,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ShardsoftheVoid3\")\nend)\n__bundle_register(\"playercards/cards/ShardsoftheVoid3\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"0\"] = true\n}\n\nSHOW_SINGLE_RELEASE = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ShardsoftheVoid3\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/ShardsoftheVoid3\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"0\"] = true\n}\n\nMAX_SEALED = 4 -- Core Set is component-limited to 4 '0' tokens\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -93736,7 +37454,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ThirdTimesaCharm\")\nend)\n__bundle_register(\"playercards/cards/ThirdTimesaCharm\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -93920,7 +37638,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between the lvl 0 and lvl 3 versions of Scroll of Secrets\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- get class via metadata and create context menu accordingly\nfunction onLoad()\n local notes = JSON.decode(self.getGMNotes())\n if notes then\n createContextMenu(notes.id)\n else\n print(\"Missing metadata for Scroll of Secrets!\")\n end\nend\n\nfunction createContextMenu(id)\n if id == \"05116\" or id == \"05116-t\" then\n -- lvl 0: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n elseif id == \"05188\" or id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n self.addContextMenuItem(\"Draw bottom card(s)\", function(playerColor) contextFunc(playerColor, 3) end)\n elseif id == \"05189\" or id == \"05189-t\" then\n -- mystic lvl 3: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n end\nend\n\nfunction contextFunc(playerColor, amount)\n local options = { \"Encounter Deck\" }\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playmatApi.getMatColor(color)\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n table.insert(options, color)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _, function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n -- variable initialization\n local deck = nil\n local deckSize = 1\n local deckAreaObjects = {}\n\n -- get the respective deck\n if owner == \"Encounter Deck\" then\n deck = mythosAreaApi.getEncounterDeck()\n else\n local matColor = playmatApi.getMatColor(owner)\n deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n deck = deckAreaObjects.draw\n end\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n -- get class via metadata and proceed menu accordingly:\n -- lvl 0: draw 1 card from the bottom\n -- mystic lvl 3: draw 1 card from the bottom\n local buttonLabel = \"Draw bottom card\"\n local amount = 1\n local notes = JSON.decode(self.getGMNotes())\n if notes.id == \"05188\" or notes.id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n buttonLabel = buttonLabel .. \"(s)\"\n amount = 3\n end\n self.addContextMenuItem(buttonLabel, function(playerColor) contextFunc(playerColor, amount) end)\nend\n\nfunction contextFunc(playerColor, amount)\n Player[playerColor].clearSelectedObjects()\n deckData = {}\n local options = {}\n\n -- check for encounter deck\n local encounterDeck = mythosAreaApi.getEncounterDeck()\n if encounterDeck then\n table.insert(options, \"Encounter Deck\")\n deckData[\"Encounter Deck\"] = { draw = encounterDeck }\n end\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playermatApi.getMatColor(color)\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n local playerName = Player[color].steam_name\n local invName = playermatApi.getInvestigatorName(matColor)\n\n -- figure out a proper display name for the drop down menu\n local displayName = color .. \" (color)\"\n if playerName then\n displayName = playerName\n elseif invName ~= \"\" then\n displayName = invName .. \" (inv)\"\n end\n\n -- recreate table in this script to have access\n deckData[displayName] = {\n draw = deckAreaObjects.draw,\n topCard = deckAreaObjects.topCard\n }\n table.insert(options, displayName)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _,\n function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n local deckAreaObjects = deckData[owner]\n local deck = deckAreaObjects.draw\n local deckSize = 1\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -94591,7 +38309,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90010\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Task.\",\n \"weakness\": true,\n \"uses\": [\n {\n \"count\": 0,\n \"type\": \"Resource\",\n \"token\": \"resource\"\n }\n ],\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90010\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Task.\",\n \"weakness\": true,\n \"uses\": [\n {\n \"count\": 0,\n \"type\": \"Resource\",\n \"token\": \"resource\"\n }\n ],\n \"cycle\": \"All or Nothing\"\n}", "GUID": "bd323d", "Grid": true, "GridProjection": false, @@ -99896,7 +43614,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/CrystallineElderSign3\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"playercards/cards/CrystallineElderSign3\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"+1\"] = true,\n [\"Elder Sign\"] = true\n}\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/CrystallineElderSign3\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"+1\"] = true,\n [\"Elder Sign\"] = true\n}\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/CrystallineElderSign3\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -100318,7 +44036,7 @@ }, "Description": "Consult Experts", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90027\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90027\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"By the Book\"\n}", "GUID": "2d9256", "Grid": true, "GridProjection": false, @@ -100389,7 +44107,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/WellConnected\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between both the level 0 and the upgraded level 3 version of the card\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nlocal modValue, loopId\nlocal buttonParameters = {\n click_function = \"toggleCounter\",\n tooltip = \"disable counter\",\n function_owner = self,\n position = { 0.88, 0.5, -1.33 },\n font_size = 150,\n width = 175,\n height = 175\n}\n\nfunction onSave() return JSON.encode({ loopId = loopId }) end\n\nfunction onLoad(savedData)\n -- use metadata to detect level and adjust modValue accordingly\n if JSON.decode(self.getGMNotes()).level == 0 then\n modValue = 5\n else\n modValue = 4\n end\n\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.loopId then\n self.createButton(buttonParameters)\n loopId = Wait.time(updateDisplay, 2, -1)\n end\n end\n\n self.addContextMenuItem(\"Toggle Counter\", toggleCounter)\nend\n\nfunction toggleCounter()\n if loopId ~= nil then\n Wait.stop(loopId)\n loopId = nil\n self.clearButtons()\n else\n self.createButton(buttonParameters)\n updateDisplay()\n loopId = Wait.time(updateDisplay, 2, -1)\n end\nend\n\nfunction updateDisplay()\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n local resources = playmatApi.getCounterValue(matColor, \"ResourceCounter\")\n local count = tostring(math.floor(resources / modValue))\n self.editButton({ index = 0, label = count })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/WellConnected\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/WellConnected\")\nend)\n__bundle_register(\"playercards/cards/WellConnected\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- intentionally global\nhasXML = false\nisHelperEnabled = false\n\nlocal modValue, loopId\n\nlocal buttonParameters = {\n click_function = \"shutOff\",\n function_owner = self,\n position = { 0.88, 0.5, -1.33 },\n font_size = 150,\n width = 175,\n height = 175\n}\n\nfunction updateSave()\n self.script_state = JSON.encode({\n isHelperEnabled = isHelperEnabled,\n loopId = loopId\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n loopId = loadedData.loopId\n end\n\n -- use metadata to detect level and adjust modValue accordingly\n if JSON.decode(self.getGMNotes()).level == 0 then\n modValue = 5\n else\n modValue = 4\n end\n\n syncDisplayWithOptionPanel()\nend\n\nfunction initialize()\n self.clearButtons()\n self.createButton(buttonParameters)\n updateButton()\n loopId = Wait.time(updateButton, 2, -1)\nend\n\nfunction shutOff()\n self.clearButtons()\n if loopId then\n Wait.stop(loopId)\n loopId = nil\n end\nend\n\nfunction updateButton()\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n local resources = playermatApi.getCounterValue(matColor, \"ResourceCounter\")\n local count = tostring(math.floor(resources / modValue))\n self.editButton({ index = 0, label = count })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -101254,7 +44972,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FalseCovenant\")\nend)\n__bundle_register(\"playercards/cards/FalseCovenant\", function(require, _LOADED, __bundle_register, __bundle_modules)\nbuttonLabel = \"Cancel\"\nbuttonIcon = \"token-curse\"\nbuttonColor = \"#633A84E6\"\nbuttonFontSize = 300\n\nRETURN_TO_POOL = true\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -104513,7 +48231,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ShieldofFaith2\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/ShieldofFaith2\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\nSHOW_SINGLE_RELEASE = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ShieldofFaith2\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/ShieldofFaith2\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\nMAX_SEALED = 5\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -105300,7 +49018,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90031\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Task.\",\n \"weakness\": true,\n \"uses\": [\n {\n \"count\": 4,\n \"type\": \"Clue\",\n \"token\": \"clue\"\n }\n ],\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90031\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Task.\",\n \"weakness\": true,\n \"uses\": [\n {\n \"count\": 4,\n \"type\": \"Clue\",\n \"token\": \"clue\"\n }\n ],\n \"cycle\": \"By the Book\"\n}", "GUID": "f802e3", "Grid": true, "GridProjection": false, @@ -105483,7 +49201,7 @@ }, "Description": "Due Diligence", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90025\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90025\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"By the Book\"\n}", "GUID": "133521", "Grid": true, "GridProjection": false, @@ -105667,7 +49385,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03265\",\n \"type\": \"Event\",\n \"class\": \"Seeker\",\n \"cost\": 0,\n \"level\": 0,\n \"traits\": \"Insight.\",\n \"wildIcons\": 1,\n \"cycle\": \"The Path to Carcosa\"\n}", + "GMNotes": "{\n \"id\": \"03265\",\n \"type\": \"Event\",\n \"class\": \"Seeker\",\n \"cost\": 0,\n \"level\": 0,\n \"traits\": \"Insight.\",\n \"wildIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Path to Carcosa\"\n}", "GUID": "bbfe9b", "Grid": true, "GridProjection": false, @@ -106345,7 +50063,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90019\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 4,\n \"traits\": \"Spell.\",\n \"weakness\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90019\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 4,\n \"traits\": \"Spell.\",\n \"weakness\": true,\n \"cycle\": \"Bad Blood\"\n}", "GUID": "580a4d", "Grid": true, "GridProjection": false, @@ -106905,7 +50623,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between the lvl 0 and lvl 3 versions of Scroll of Secrets\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- get class via metadata and create context menu accordingly\nfunction onLoad()\n local notes = JSON.decode(self.getGMNotes())\n if notes then\n createContextMenu(notes.id)\n else\n print(\"Missing metadata for Scroll of Secrets!\")\n end\nend\n\nfunction createContextMenu(id)\n if id == \"05116\" or id == \"05116-t\" then\n -- lvl 0: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n elseif id == \"05188\" or id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n self.addContextMenuItem(\"Draw bottom card(s)\", function(playerColor) contextFunc(playerColor, 3) end)\n elseif id == \"05189\" or id == \"05189-t\" then\n -- mystic lvl 3: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n end\nend\n\nfunction contextFunc(playerColor, amount)\n local options = { \"Encounter Deck\" }\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playmatApi.getMatColor(color)\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n table.insert(options, color)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _, function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n -- variable initialization\n local deck = nil\n local deckSize = 1\n local deckAreaObjects = {}\n\n -- get the respective deck\n if owner == \"Encounter Deck\" then\n deck = mythosAreaApi.getEncounterDeck()\n else\n local matColor = playmatApi.getMatColor(owner)\n deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n deck = deckAreaObjects.draw\n end\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n -- get class via metadata and proceed menu accordingly:\n -- lvl 0: draw 1 card from the bottom\n -- mystic lvl 3: draw 1 card from the bottom\n local buttonLabel = \"Draw bottom card\"\n local amount = 1\n local notes = JSON.decode(self.getGMNotes())\n if notes.id == \"05188\" or notes.id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n buttonLabel = buttonLabel .. \"(s)\"\n amount = 3\n end\n self.addContextMenuItem(buttonLabel, function(playerColor) contextFunc(playerColor, amount) end)\nend\n\nfunction contextFunc(playerColor, amount)\n Player[playerColor].clearSelectedObjects()\n deckData = {}\n local options = {}\n\n -- check for encounter deck\n local encounterDeck = mythosAreaApi.getEncounterDeck()\n if encounterDeck then\n table.insert(options, \"Encounter Deck\")\n deckData[\"Encounter Deck\"] = { draw = encounterDeck }\n end\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playermatApi.getMatColor(color)\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n local playerName = Player[color].steam_name\n local invName = playermatApi.getInvestigatorName(matColor)\n\n -- figure out a proper display name for the drop down menu\n local displayName = color .. \" (color)\"\n if playerName then\n displayName = playerName\n elseif invName ~= \"\" then\n displayName = invName .. \" (inv)\"\n end\n\n -- recreate table in this script to have access\n deckData[displayName] = {\n draw = deckAreaObjects.draw,\n topCard = deckAreaObjects.topCard\n }\n table.insert(options, displayName)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _,\n function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n local deckAreaObjects = deckData[owner]\n local deck = deckAreaObjects.draw\n local deckSize = 1\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -108998,7 +52716,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/RiteofSanctification\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\nSHOW_SINGLE_RELEASE = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/RiteofSanctification\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/RiteofSanctification\")\nend)\n__bundle_register(\"playercards/cards/RiteofSanctification\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\nMAX_SEALED = 5\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -109359,7 +53077,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"07108\",\n \"type\": \"Asset\",\n \"slot\": \"Accessory\",\n \"class\": \"Guardian\",\n \"cost\": 2,\n \"level\": 0,\n \"traits\": \"Item. Tool.\",\n \"willpowerIcons\": 1,\n \"cycle\": \"The Innsmouth Conspiracy\"\n}", + "GMNotes": "{\n \"id\": \"07108\",\n \"type\": \"Asset\",\n \"slot\": \"Accessory\",\n \"class\": \"Guardian\",\n \"cost\": 2,\n \"level\": 0,\n \"traits\": \"Item. Tool.\",\n \"willpowerIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Engage\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Innsmouth Conspiracy\"\n}", "GUID": "55fc3d", "Grid": true, "GridProjection": false, @@ -109615,7 +53333,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/TheCodexofAges\")\nend)\n__bundle_register(\"playercards/cards/TheCodexofAges\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Elder Sign\"] = true\n}\n\nRESOLVE_TOKEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/TheCodexofAges\")\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"playercards/cards/TheCodexofAges\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Elder Sign\"] = true\n}\n\nRESOLVE_TOKEN = true\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -110602,7 +54320,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ProtectiveIncantation1\")\nend)\n__bundle_register(\"playercards/cards/ProtectiveIncantation1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {}\n\nINVALID_TOKENS = {\n [\"Auto-fail\"] = true\n}\n\nUPDATE_ON_HOVER = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/ProtectiveIncantation1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {}\n\nINVALID_TOKENS = {\n [\"Auto-fail\"] = true\n}\n\nUPDATE_ON_HOVER = true\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ProtectiveIncantation1\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -111218,7 +54936,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FavoroftheMoon1\")\nend)\n__bundle_register(\"playercards/cards/FavoroftheMoon1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nSHOW_SINGLE_RELEASE = true\nKEEP_OPEN = true\nRESOLVE_TOKEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FavoroftheMoon1\")\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/FavoroftheMoon1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nKEEP_OPEN = true\nMAX_SEALED = 3\nRESOLVE_TOKEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -112133,7 +55851,7 @@ }, "Description": "Red Tape", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90026\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90026\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"By the Book\"\n}", "GUID": "706176", "Grid": true, "GridProjection": false, @@ -112204,7 +55922,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between the lvl 0 and lvl 3 versions of Scroll of Secrets\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- get class via metadata and create context menu accordingly\nfunction onLoad()\n local notes = JSON.decode(self.getGMNotes())\n if notes then\n createContextMenu(notes.id)\n else\n print(\"Missing metadata for Scroll of Secrets!\")\n end\nend\n\nfunction createContextMenu(id)\n if id == \"05116\" or id == \"05116-t\" then\n -- lvl 0: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n elseif id == \"05188\" or id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n self.addContextMenuItem(\"Draw bottom card(s)\", function(playerColor) contextFunc(playerColor, 3) end)\n elseif id == \"05189\" or id == \"05189-t\" then\n -- mystic lvl 3: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n end\nend\n\nfunction contextFunc(playerColor, amount)\n local options = { \"Encounter Deck\" }\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playmatApi.getMatColor(color)\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n table.insert(options, color)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _, function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n -- variable initialization\n local deck = nil\n local deckSize = 1\n local deckAreaObjects = {}\n\n -- get the respective deck\n if owner == \"Encounter Deck\" then\n deck = mythosAreaApi.getEncounterDeck()\n else\n local matColor = playmatApi.getMatColor(owner)\n deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n deck = deckAreaObjects.draw\n end\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n -- get class via metadata and proceed menu accordingly:\n -- lvl 0: draw 1 card from the bottom\n -- mystic lvl 3: draw 1 card from the bottom\n local buttonLabel = \"Draw bottom card\"\n local amount = 1\n local notes = JSON.decode(self.getGMNotes())\n if notes.id == \"05188\" or notes.id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n buttonLabel = buttonLabel .. \"(s)\"\n amount = 3\n end\n self.addContextMenuItem(buttonLabel, function(playerColor) contextFunc(playerColor, amount) end)\nend\n\nfunction contextFunc(playerColor, amount)\n Player[playerColor].clearSelectedObjects()\n deckData = {}\n local options = {}\n\n -- check for encounter deck\n local encounterDeck = mythosAreaApi.getEncounterDeck()\n if encounterDeck then\n table.insert(options, \"Encounter Deck\")\n deckData[\"Encounter Deck\"] = { draw = encounterDeck }\n end\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playermatApi.getMatColor(color)\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n local playerName = Player[color].steam_name\n local invName = playermatApi.getInvestigatorName(matColor)\n\n -- figure out a proper display name for the drop down menu\n local displayName = color .. \" (color)\"\n if playerName then\n displayName = playerName\n elseif invName ~= \"\" then\n displayName = invName .. \" (inv)\"\n end\n\n -- recreate table in this script to have access\n deckData[displayName] = {\n draw = deckAreaObjects.draw,\n topCard = deckAreaObjects.topCard\n }\n table.insert(options, displayName)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _,\n function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n local deckAreaObjects = deckData[owner]\n local deck = deckAreaObjects.draw\n local deckSize = 1\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -112266,7 +55984,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Nephthys4\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"playercards/cards/Nephthys4\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nSHOW_SINGLE_RELEASE = true\nSHOW_MULTI_RELEASE = 3\nSHOW_MULTI_RETURN = 3\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Nephthys4\")\nend)\n__bundle_register(\"playercards/cards/Nephthys4\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\nSHOW_MULTI_RELEASE = 3\nSHOW_MULTI_RETURN = 3\nMAX_SEALED = 10\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -113120,7 +56838,7 @@ }, "Description": "Artifact from Another Life (Advanced)", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90018\",\n \"type\": \"Asset\",\n \"slot\": \"Accessory\",\n \"class\": \"Neutral\",\n \"cost\": 3,\n \"traits\": \"Item. Relic.\",\n \"willpowerIcons\": 1,\n \"combatIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90018\",\n \"type\": \"Asset\",\n \"slot\": \"Accessory\",\n \"class\": \"Neutral\",\n \"cost\": 3,\n \"traits\": \"Item. Relic.\",\n \"willpowerIcons\": 1,\n \"combatIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"Bad Blood\"\n}", "GUID": "bf151d", "Grid": true, "GridProjection": false, @@ -113191,7 +56909,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Unrelenting1\")\nend)\n__bundle_register(\"playercards/cards/Unrelenting1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {}\nINVALID_TOKENS = {\n [\"Auto-fail\"] = true\n}\n\nUPDATE_ON_HOVER = true\nKEEP_OPEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Unrelenting1\")\nend)\n__bundle_register(\"playercards/cards/Unrelenting1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {}\nINVALID_TOKENS = {\n [\"Auto-fail\"] = true\n}\n\nUPDATE_ON_HOVER = true\nKEEP_OPEN = true\nMAX_SEALED = 3\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -116386,7 +60104,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/TheChthonianStone3\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n}\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/TheChthonianStone3\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/TheChthonianStone\")\nend)\n__bundle_register(\"playercards/cards/TheChthonianStone\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n}\n\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -116439,7 +60157,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90009\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 0,\n \"traits\": \"Tactic.\",\n \"intellectIcons\": 1,\n \"agilityIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90009\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 0,\n \"traits\": \"Tactic.\",\n \"intellectIcons\": 1,\n \"agilityIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"All or Nothing\"\n}", "GUID": "9c4900", "Grid": true, "GridProjection": false, @@ -120501,7 +64219,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90002\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90002\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"Read or Die\"\n}", "GUID": "cf41be", "Grid": true, "GridProjection": false, @@ -126228,7 +69946,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"05156\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 2,\n \"level\": 0,\n \"traits\": \"Gambit.\",\n \"agilityIcons\": 2,\n \"cycle\": \"The Circle Undone\"\n}", + "GMNotes": "{\n \"id\": \"05156\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 2,\n \"level\": 0,\n \"traits\": \"Gambit.\",\n \"agilityIcons\": 2,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Circle Undone\"\n}", "GUID": "c2d211", "Grid": true, "GridProjection": false, @@ -127649,7 +71367,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FavoroftheSun1\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/FavoroftheSun1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nSHOW_SINGLE_RELEASE = true\nKEEP_OPEN = true\nRESOLVE_TOKEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FavoroftheSun1\")\nend)\n__bundle_register(\"playercards/cards/FavoroftheSun1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\nMAX_SEALED = 3\nRESOLVE_TOKEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -128391,7 +72109,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/WellConnected\")\nend)\n__bundle_register(\"playercards/cards/WellConnected\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between both the level 0 and the upgraded level 3 version of the card\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nlocal modValue, loopId\nlocal buttonParameters = {\n click_function = \"toggleCounter\",\n tooltip = \"disable counter\",\n function_owner = self,\n position = { 0.88, 0.5, -1.33 },\n font_size = 150,\n width = 175,\n height = 175\n}\n\nfunction onSave() return JSON.encode({ loopId = loopId }) end\n\nfunction onLoad(savedData)\n -- use metadata to detect level and adjust modValue accordingly\n if JSON.decode(self.getGMNotes()).level == 0 then\n modValue = 5\n else\n modValue = 4\n end\n\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.loopId then\n self.createButton(buttonParameters)\n loopId = Wait.time(updateDisplay, 2, -1)\n end\n end\n\n self.addContextMenuItem(\"Toggle Counter\", toggleCounter)\nend\n\nfunction toggleCounter()\n if loopId ~= nil then\n Wait.stop(loopId)\n loopId = nil\n self.clearButtons()\n else\n self.createButton(buttonParameters)\n updateDisplay()\n loopId = Wait.time(updateDisplay, 2, -1)\n end\nend\n\nfunction updateDisplay()\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n local resources = playmatApi.getCounterValue(matColor, \"ResourceCounter\")\n local count = tostring(math.floor(resources / modValue))\n self.editButton({ index = 0, label = count })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/WellConnected\")\nend)\n__bundle_register(\"playercards/cards/WellConnected\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- intentionally global\nhasXML = false\nisHelperEnabled = false\n\nlocal modValue, loopId\n\nlocal buttonParameters = {\n click_function = \"shutOff\",\n function_owner = self,\n position = { 0.88, 0.5, -1.33 },\n font_size = 150,\n width = 175,\n height = 175\n}\n\nfunction updateSave()\n self.script_state = JSON.encode({\n isHelperEnabled = isHelperEnabled,\n loopId = loopId\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n loopId = loadedData.loopId\n end\n\n -- use metadata to detect level and adjust modValue accordingly\n if JSON.decode(self.getGMNotes()).level == 0 then\n modValue = 5\n else\n modValue = 4\n end\n\n syncDisplayWithOptionPanel()\nend\n\nfunction initialize()\n self.clearButtons()\n self.createButton(buttonParameters)\n updateButton()\n loopId = Wait.time(updateButton, 2, -1)\nend\n\nfunction shutOff()\n self.clearButtons()\n if loopId then\n Wait.stop(loopId)\n loopId = nil\n end\nend\n\nfunction updateButton()\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n local resources = playermatApi.getCounterValue(matColor, \"ResourceCounter\")\n local count = tostring(math.floor(resources / modValue))\n self.editButton({ index = 0, label = count })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -129191,7 +72909,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/TheChthonianStone\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/cards/TheChthonianStone\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n}\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/TheChthonianStone\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/TheChthonianStone\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n}\n\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -129491,7 +73209,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02266\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 0,\n \"level\": 3,\n \"traits\": \"Trick.\",\n \"cycle\": \"The Dunwich Legacy\"\n}", + "GMNotes": "{\n \"id\": \"02266\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 0,\n \"level\": 3,\n \"traits\": \"Trick.\",\n \"uses\": [\n {\n \"count\": 3,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Dunwich Legacy\"\n}", "GUID": "074858", "Grid": true, "GridProjection": false, @@ -130785,7 +74503,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"04148\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Tome.\",\n \"intellectIcons\": 2,\n \"cycle\": \"The Forgotten Age\"\n}", + "GMNotes": "{\n \"id\": \"04148\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Tome.\",\n \"intellectIcons\": 2,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Explore\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Forgotten Age\"\n}", "GUID": "9dc3d4", "Grid": true, "GridProjection": false, @@ -134857,7 +78575,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/HolySpear5\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nSHOW_SINGLE_RELEASE = true\nSHOW_MULTI_SEAL = 2\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/HolySpear5\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/HolySpear5\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nSHOW_MULTI_SEAL = 2\nMAX_SEALED = 10\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/HolySpear5\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -140697,7 +84415,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"06028\",\n \"type\": \"Event\",\n \"class\": \"Mystic\",\n \"traits\": \"Augury.\",\n \"cycle\": \"The Dream-Eaters\"\n}", + "GMNotes": "{\n \"id\": \"06028\",\n \"type\": \"Event\",\n \"class\": \"Mystic\",\n \"traits\": \"Augury.\",\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Dream-Eaters\"\n}", "GUID": "600a3c", "Grid": true, "GridProjection": false, @@ -141875,7 +85593,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/SerpentsofYig\")\nend)\n__bundle_register(\"playercards/cards/SerpentsofYig\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Elder Sign\"] = true\n}\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/SerpentsofYig\")\nend)\n__bundle_register(\"playercards/cards/SerpentsofYig\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Elder Sign\"] = true\n}\n\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -141927,7 +85645,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90030\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Neutral\",\n \"cost\": 3,\n \"traits\": \"Item. Weapon. Firearm.\",\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"wildIcons\": 1,\n \"uses\": [\n {\n \"count\": 4,\n \"type\": \"Ammo\",\n \"token\": \"resource\"\n }\n ],\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90030\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Neutral\",\n \"cost\": 3,\n \"traits\": \"Item. Weapon. Firearm.\",\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"wildIcons\": 1,\n \"uses\": [\n {\n \"count\": 4,\n \"type\": \"Ammo\",\n \"token\": \"resource\"\n }\n ],\n \"cycle\": \"By the Book\"\n}", "GUID": "dbdaff", "Grid": true, "GridProjection": false, @@ -142366,7 +86084,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/SealoftheSeventhSign5\")\nend)\n__bundle_register(\"playercards/cards/SealoftheSeventhSign5\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Auto-fail\"] = true\n}\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/SealoftheSeventhSign5\")\nend)\n__bundle_register(\"playercards/cards/SealoftheSeventhSign5\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Auto-fail\"] = true\n}\n\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -143288,7 +87006,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/RadiantSmite1\")\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/RadiantSmite1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"playercards/cards/RadiantSmite1\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Bless\"] = true\n}\n\nKEEP_OPEN = true\nMAX_SEALED = 3\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/RadiantSmite1\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -144825,7 +88543,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"playercards/cards/DayofReckoning\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Elder Sign\"] = true\n}\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/DayofReckoning\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/DayofReckoning\")\nend)\n__bundle_register(\"playercards/cards/DayofReckoning\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Elder Sign\"] = true\n}\n\nMAX_SEALED = 1\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -144939,7 +88657,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"60121\",\n \"type\": \"Event\",\n \"class\": \"Guardian\",\n \"cost\": 2,\n \"level\": 1,\n \"traits\": \"Spirit.\",\n \"willpowerIcons\": 2,\n \"cycle\": \"Investigator Packs\"\n}", + "GMNotes": "{\n \"id\": \"60121\",\n \"type\": \"Event\",\n \"class\": \"Guardian\",\n \"cost\": 2,\n \"level\": 1,\n \"traits\": \"Spirit.\",\n \"willpowerIcons\": 2,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Fight\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"Investigator Packs\"\n}", "GUID": "9e7f6a", "Grid": true, "GridProjection": false, @@ -146726,7 +90444,7 @@ }, "Description": "Seek the Truth", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90028\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90028\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"By the Book\"\n}", "GUID": "0994c9", "Grid": true, "GridProjection": false, @@ -147095,7 +90813,7 @@ }, "Description": "Leave No Doubt", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90029\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90029\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"By the Book\"\n}", "GUID": "07e7bd", "Grid": true, "GridProjection": false, @@ -147966,7 +91684,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/DarkRitual\")\nend)\n__bundle_register(\"playercards/cards/DarkRitual\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nKEEP_OPEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/DarkRitual\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"playercards/cards/DarkRitual\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nKEEP_OPEN = true\nMAX_SEALED = 5\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -148211,7 +91929,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FamilyInheritance\")\nend)\n__bundle_register(\"playercards/cards/FamilyInheritance\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\nlocal clickableResourceCounter = nil\nlocal foundTokens = 0\n\nfunction onLoad()\n self.addContextMenuItem(\"Add 4 resources\", function(playerColor) add4(playerColor) end)\n self.addContextMenuItem(\"Take all resources\", function(playerColor) takeAll(playerColor) end)\n self.addContextMenuItem(\"Discard all resources\", function(playerColor) loseAll(playerColor) end)\nend\n\nfunction searchSelf()\n clickableResourceCounter = nil\n foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(self, \"isTileOrToken\")) do\n local image = obj.getCustomObject().image\n if image == \"http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/\" then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif obj.getMemo() == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n return\n end\n end\nend\n\nfunction add4(playerColor)\n searchSelf()\n\n local newCount = foundTokens + 4\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n if newCount \u003e 12 then\n printToColor(\"Count increased to \" .. newCount .. \" resources. Spawning clickable counter instead.\", playerColor)\n tokenManager.spawnResourceCounterToken(self, newCount)\n else\n tokenManager.spawnTokenGroup(self, \"resource\", newCount)\n end\n end\nend\n\nfunction takeAll(playerColor)\n searchSelf()\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n playmatApi.updateCounter(matColor, \"ResourceCounter\", _, foundTokens)\n\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", 0)\n end\n printToColor(\"Moved \" .. foundTokens .. \" resource(s) to \" .. matColor .. \"'s resource pool.\", playerColor)\nend\n\nfunction loseAll(playerColor)\n searchSelf()\n\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", 0)\n end\n printToColor(\"Discarded \" .. foundTokens .. \" resource(s).\", playerColor)\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn, for example \"damage\", \"horror\" or \"resource\"\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string type of token to spawn, valid values are \"damage\" and \"horror\". Other\n -- types should use spawnMultipleTokens()\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string type of token to spawn, valid values are resource\", \"doom\", or \"clue\".\n -- Other types should use spawnCounterToken()\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then return end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- handling for not provided subtype (for example when spawning from custom data helpers)\n if subType == nil then\n subType = \"\"\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType)]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string type of token to spawn, valid values are \"damage\", \"horror\",\n -- \"resource\", \"doom\", or \"clue\"\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n TokenManager.maybeReplenishCard = function(card, uses, mat)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses, mat)\n end\n end\n\n -- Delegate function to the token spawn tracker. Exists to avoid circular dependencies in some\n -- callers.\n ---@param card tts__Object Card object to reset the tokens for\n TokenManager.resetTokensSpawned = function(card)\n tokenSpawnTrackerApi.resetTokensSpawned(card.getGUID())\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playmat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n ---@param mat tts__Object The playmat the card is placed on (for rotation and casting)\n internal.replenishTokens = function(card, uses, mat)\n local cardPos = card.getPosition()\n\n -- don't continue for cards on the deck (Norman) or in the discard pile\n if mat.positionToLocal(cardPos).x \u003c -1 then return end\n\n -- get current amount of resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if (stateTable[memo] or 0) \u003e 0 then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FamilyInheritance\")\nend)\n__bundle_register(\"core/token/TokenManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local optionPanelApi = require(\"core/OptionPanelApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n local playermatApi = require(\"playermat/PlayermatApi\")\n local searchLib = require(\"util/SearchLib\")\n local tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n local PLAYER_CARD_TOKEN_OFFSETS = {\n [1] = {\n Vector(0, 3, -0.2)\n },\n [2] = {\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [3] = {\n Vector(0, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [4] = {\n Vector(0.4, 3, -0.9),\n Vector(-0.4, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [5] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.4, 3, -0.2),\n Vector(-0.4, 3, -0.2)\n },\n [6] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2)\n },\n [7] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0, 3, 0.5)\n },\n [8] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(-0.35, 3, 0.5),\n Vector(0.35, 3, 0.5)\n },\n [9] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5)\n },\n [10] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0, 3, 1.2)\n },\n [11] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(-0.35, 3, 1.2),\n Vector(0.35, 3, 1.2)\n },\n [12] = {\n Vector(0.7, 3, -0.9),\n Vector(0, 3, -0.9),\n Vector(-0.7, 3, -0.9),\n Vector(0.7, 3, -0.2),\n Vector(0, 3, -0.2),\n Vector(-0.7, 3, -0.2),\n Vector(0.7, 3, 0.5),\n Vector(0, 3, 0.5),\n Vector(-0.7, 3, 0.5),\n Vector(0.7, 3, 1.2),\n Vector(0, 3, 1.2),\n Vector(-0.7, 3, 1.2)\n }\n }\n\n -- stateIDs for the multi-stated resource tokens\n local stateTable = {\n [\"resource\"] = 1,\n [\"ammo\"] = 2,\n [\"bounty\"] = 3,\n [\"charge\"] = 4,\n [\"evidence\"] = 5,\n [\"secret\"] = 6,\n [\"supply\"] = 7,\n [\"offering\"] = 8\n }\n\n -- Table of data extracted from the token source bag, keyed by the Memo on each token which\n -- should match the token type keys (\"resource\", \"clue\", etc)\n local tokenTemplates\n\n local playerCardData\n local locationData\n\n local TokenManager = {}\n local internal = {}\n\n -- Spawns tokens for the card. This function is built to just throw a card at it and let it do\n -- the work once a card has hit an area where it might spawn tokens. It will check to see if\n -- the card has already spawned, find appropriate data from either the uses metadata or the Data\n -- Helper, and spawn the tokens.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n TokenManager.spawnForCard = function(card, extraUses)\n if tokenSpawnTrackerApi.hasSpawnedTokens(card.getGUID()) then\n return\n end\n local metadata = JSON.decode(card.getGMNotes())\n if metadata ~= nil then\n internal.spawnTokensFromUses(card, extraUses)\n else\n internal.spawnTokensFromDataHelper(card)\n end\n end\n\n -- Spawns a set of tokens on the given card.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn. For damage or horror this value will be set to the\n -- spawned state object rather than spawning multiple tokens\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource tokens\n TokenManager.spawnTokenGroup = function(card, tokenType, tokenCount, shiftDown, subType)\n local optionPanel = optionPanelApi.getOptions()\n\n if tokenType == \"damage\" or tokenType == \"horror\" then\n TokenManager.spawnCounterToken(card, tokenType, tokenCount, shiftDown)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"enabled\" then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n elseif tokenType == \"resource\" and optionPanel[\"useResourceCounters\"] == \"custom\" and tokenCount == 0 then\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n else\n TokenManager.spawnMultipleTokens(card, tokenType, tokenCount, shiftDown, subType)\n end\n end\n\n -- Spawns a single counter token and sets the value to tokenValue. Used for damage and horror tokens.\n ---@param card tts__Object Card to spawn tokens on\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenValue number Value to set the damage/horror to\n TokenManager.spawnCounterToken = function(card, tokenType, tokenValue, shiftDown)\n if tokenValue \u003c 1 or tokenValue \u003e 50 then return end\n\n local pos = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[1][1] + Vector(0, 0, shiftDown))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, tokenType, rot, function(spawned)\n -- token starts in state 1, so don't attempt to change it to avoid error\n if tokenValue ~= 1 then\n spawned.setState(tokenValue)\n end\n end)\n end\n\n TokenManager.spawnResourceCounterToken = function(card, tokenCount)\n local pos = card.positionToWorld(card.positionToLocal(card.getPosition()) + Vector(0, 0.2, -0.5))\n local rot = card.getRotation()\n TokenManager.spawnToken(pos, \"resourceCounter\", rot, function(spawned)\n spawned.call(\"updateVal\", tokenCount)\n end)\n end\n\n -- Spawns a number of tokens.\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param tokenCount number How many tokens to spawn\n ---@param shiftDown? number An offset for the z-value of this group of tokens\n ---@param subType? string Subtype of token to spawn. This will only differ from the tokenName for resource or action tokens\n TokenManager.spawnMultipleTokens = function(card, tokenType, tokenCount, shiftDown, subType)\n -- not checking the max at this point since clue offsets are calculated dynamically\n if tokenCount \u003c 1 then return end\n\n local offsets = {}\n if tokenType == \"clue\" then\n offsets = internal.buildClueOffsets(card, tokenCount)\n else\n -- only up to 12 offset tables defined\n if tokenCount \u003e 12 then\n printToAll(\"Attempting to spawn \" .. tokenCount .. \" tokens. Spawning clickable counter instead.\")\n TokenManager.spawnResourceCounterToken(card, tokenCount)\n return\n end\n for i = 1, tokenCount do\n offsets[i] = card.positionToWorld(PLAYER_CARD_TOKEN_OFFSETS[tokenCount][i])\n -- Fix the y-position for the spawn, since positionToWorld considers rotation which can\n -- have bad results for face up/down differences\n offsets[i].y = card.getPosition().y + 0.15\n end\n end\n\n if shiftDown ~= nil then\n -- Copy the offsets to make sure we don't change the static values\n local baseOffsets = offsets\n offsets = {}\n\n -- get a vector for the shifting (downwards local to the card)\n local shiftDownVector = Vector(0, 0, shiftDown):rotateOver(\"y\", card.getRotation().y)\n for i, baseOffset in ipairs(baseOffsets) do\n offsets[i] = baseOffset + shiftDownVector\n end\n end\n\n if offsets == nil then\n error(\"couldn't find offsets for \" .. tokenCount .. ' tokens')\n return\n end\n\n -- this is used to load the correct state for additional resource tokens (e.g. \"Ammo\")\n local callback = nil\n local stateID = stateTable[string.lower(subType or \"\")]\n if tokenType == \"resource\" and stateID ~= nil and stateID ~= 1 then\n callback = function(spawned) spawned.setState(stateID) end\n elseif tokenType == \"universalActionAbility\" then\n local matColor = playermatApi.getMatColorByPosition(card.getPosition())\n local class = playermatApi.returnInvestigatorClass(matColor)\n\n callback = function(spawned) spawned.call(\"updateClassAndSymbol\", { class = class, symbol = subType or class }) end\n end\n\n for i = 1, tokenCount do\n TokenManager.spawnToken(offsets[i], tokenType, card.getRotation(), callback)\n end\n end\n\n -- Spawns a single token at the given global position by copying it from the template bag.\n ---@param position tts__Vector Global position to spawn the token\n ---@param tokenType string Type of token to spawn (template needs to be in source bag)\n ---@param rotation tts__Vector Rotation to be used for the new token. Only the y-value will be used,\n -- x and z will use the default rotation from the source bag\n ---@param callback? function A callback function triggered after the new token is spawned\n TokenManager.spawnToken = function(position, tokenType, rotation, callback)\n internal.initTokenTemplates()\n local loadTokenType = tokenType\n if tokenType == \"clue\" or tokenType == \"doom\" then\n loadTokenType = \"clueDoom\"\n end\n if tokenTemplates[loadTokenType] == nil then\n error(\"Unknown token type '\" .. tokenType .. \"'\")\n return\n end\n local tokenTemplate = tokenTemplates[loadTokenType]\n\n -- Take ONLY the Y-value for rotation, so we don't flip the token coming out of the bag\n local rot = Vector(tokenTemplate.Transform.rotX, 270, tokenTemplate.Transform.rotZ)\n if rotation ~= nil then\n rot.y = rotation.y\n end\n if tokenType == \"doom\" then\n rot.z = 180\n end\n\n tokenTemplate.Nickname = \"\"\n return spawnObjectData({\n data = tokenTemplate,\n position = position,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- Checks a card for metadata to maybe replenish it\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n TokenManager.maybeReplenishCard = function(card, uses)\n -- TODO: support for cards with multiple uses AND replenish (as of yet, no official card needs that)\n if uses[1].count and uses[1].replenish then\n internal.replenishTokens(card, uses)\n end\n end\n\n -- Pushes new player card data into the local copy of the Data Helper player data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addPlayerCardData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n playerCardData[k] = v\n end\n end\n\n -- Pushes new location data into the local copy of the Data Helper location data.\n ---@param dataTable table Key/Value pairs following the DataHelper style\n TokenManager.addLocationData = function(dataTable)\n internal.initDataHelperData()\n for k, v in pairs(dataTable) do\n locationData[k] = v\n end\n end\n\n -- Checks to see if the given card has location data in the DataHelper\n ---@param card tts__Object Card to check for data\n ---@return boolean: True if this card has data in the helper, false otherwise\n TokenManager.hasLocationData = function(card)\n internal.initDataHelperData()\n return internal.getLocationData(card) ~= nil\n end\n\n internal.initTokenTemplates = function()\n if tokenTemplates ~= nil then\n return\n end\n tokenTemplates = {}\n local tokenSource = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSource\")\n for _, tokenTemplate in ipairs(tokenSource.getData().ContainedObjects) do\n local tokenName = tokenTemplate.Memo\n tokenTemplates[tokenName] = tokenTemplate\n end\n end\n\n -- Copies the data from the DataHelper. Will only happen once.\n internal.initDataHelperData = function()\n if playerCardData ~= nil then\n return\n end\n local dataHelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n playerCardData = dataHelper.getTable('PLAYER_CARD_DATA')\n locationData = dataHelper.getTable('LOCATIONS_DATA')\n end\n\n -- Spawn tokens for a card based on the uses metadata. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param extraUses table A table of \u003cuse type\u003e=\u003ccount\u003e which will modify the number of tokens\n --- spawned for that type. e.g. Akachi's playermat should pass \"Charge\"=1\n internal.spawnTokensFromUses = function(card, extraUses)\n local uses = internal.getUses(card)\n if uses == nil then return end\n\n -- go through tokens to spawn\n local tokenCount\n for i, useInfo in ipairs(uses) do\n tokenCount = (useInfo.count or 0) + (useInfo.countPerInvestigator or 0) * playAreaApi.getInvestigatorCount()\n if extraUses ~= nil and extraUses[useInfo.type] ~= nil then\n tokenCount = tokenCount + extraUses[useInfo.type]\n end\n -- Shift each spawned group after the first down so they don't pile on each other\n TokenManager.spawnTokenGroup(card, useInfo.token, tokenCount, (i - 1) * 0.8, useInfo.type)\n end\n\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a card based on the data helper data. This will consider the face up/down state\n -- of the card for both locations and standard cards.\n ---@param card tts__Object Card to maybe spawn tokens for\n internal.spawnTokensFromDataHelper = function(card)\n internal.initDataHelperData()\n local playerData = internal.getPlayerCardData(card)\n if playerData ~= nil then\n internal.spawnPlayerCardTokensFromDataHelper(card, playerData)\n end\n local locationData = internal.getLocationData(card)\n if locationData ~= nil then\n internal.spawnLocationTokensFromDataHelper(card, locationData)\n end\n end\n\n -- Spawn tokens for a player card using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param playerData table Player card data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnPlayerCardTokensFromDataHelper = function(card, playerData)\n local token = playerData.tokenType\n local tokenCount = playerData.tokenCount\n TokenManager.spawnTokenGroup(card, token, tokenCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n\n -- Spawn tokens for a location using data retrieved from the Data Helper.\n ---@param card tts__Object Card to maybe spawn tokens for\n ---@param locationData table Location data structure retrieved from the DataHelper. Should be\n -- the right data for this card.\n internal.spawnLocationTokensFromDataHelper = function(card, locationData)\n local clueCount = internal.getClueCountFromData(card, locationData)\n if clueCount \u003e 0 then\n TokenManager.spawnTokenGroup(card, \"clue\", clueCount)\n tokenSpawnTrackerApi.markTokensSpawned(card.getGUID())\n end\n end\n\n internal.getPlayerCardData = function(card)\n return playerCardData[card.getName() .. ':' .. card.getDescription()]\n or playerCardData[card.getName()]\n end\n\n internal.getLocationData = function(card)\n return locationData[card.getName() .. '_' .. card.getGUID()] or locationData[card.getName()]\n end\n\n internal.getClueCountFromData = function(card, locationData)\n -- Return the number of clues to spawn on this location\n if locationData == nil then\n error('attempted to get clue for unexpected object: ' .. card.getName())\n return 0\n end\n\n if ((card.is_face_down and locationData.clueSide == 'back')\n or (not card.is_face_down and locationData.clueSide == 'front')) then\n if locationData.type == 'fixed' then\n return locationData.value\n elseif locationData.type == 'perPlayer' then\n return locationData.value * playAreaApi.getInvestigatorCount()\n end\n error('unexpected location type: ' .. locationData.type)\n end\n return 0\n end\n\n -- Gets the right uses structure for this card, based on metadata and face up/down state\n ---@param card tts__Object Card to pull the uses from\n internal.getUses = function(card)\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.type == \"Location\" then\n if card.is_face_down and metadata.locationBack ~= nil then\n return metadata.locationBack.uses\n elseif not card.is_face_down and metadata.locationFront ~= nil then\n return metadata.locationFront.uses\n end\n elseif not card.is_face_down then\n return metadata.uses\n end\n\n return nil\n end\n\n -- Dynamically create positions for clues on a card.\n ---@param card tts__Object Card the clues will be placed on\n ---@param count number How many clues?\n ---@return table: Array of global positions to spawn the clues at\n internal.buildClueOffsets = function(card, count)\n local cluePositions = {}\n for i = 1, count do\n local row = math.floor(1 + (i - 1) / 4)\n local column = (i - 1) % 4\n local cluePos = card.positionToWorld(Vector(-0.825 + 0.55 * column, 0, -1.5 + 0.55 * row))\n cluePos.y = cluePos.y + 0.05\n table.insert(cluePositions, cluePos)\n end\n return cluePositions\n end\n\n ---@param card tts__Object Card object to be replenished\n ---@param uses table The already decoded metadata.uses (to avoid decoding again)\n internal.replenishTokens = function(card, uses)\n -- get current amount of matching resource tokens on the card\n local clickableResourceCounter = nil\n local foundTokens = 0\n local searchType = string.lower(uses[1].type)\n\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n local memo = obj.getMemo()\n\n if searchType == memo then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif memo == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n break\n end\n end\n\n -- this is the theoretical new amount of uses (to be checked below)\n local newCount = foundTokens + uses[1].replenish\n\n -- if there are already more uses than the replenish amount, keep them\n if foundTokens \u003e uses[1].count then\n newCount = foundTokens\n -- only replenish up until the replenish amount\n elseif newCount \u003e uses[1].count then\n newCount = uses[1].count\n end\n\n -- update the clickable counter or spawn a group of tokens\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n TokenManager.spawnTokenGroup(card, uses[1].token, newCount, _, uses[1].type)\n end\n end\n\n return TokenManager\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/cards/FamilyInheritance\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenManager = require(\"core/token/TokenManager\")\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\nlocal clickableResourceCounter = nil\nlocal foundTokens = 0\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n syncDisplayWithOptionPanel()\nend\n\nfunction searchSelf()\n clickableResourceCounter = nil\n foundTokens = 0\n\n for _, obj in ipairs(searchLib.onObject(self, \"isTileOrToken\")) do\n local image = obj.getCustomObject().image\n if image == \"http://cloud-3.steamusercontent.com/ugc/1758068501357192910/11DDDC7EF621320962FDCF3AE3211D5EDC3D1573/\" then\n foundTokens = foundTokens + math.abs(obj.getQuantity())\n obj.destruct()\n elseif obj.getMemo() == \"resourceCounter\" then\n foundTokens = obj.getVar(\"val\")\n clickableResourceCounter = obj\n return\n end\n end\nend\n\nfunction add4()\n searchSelf()\n\n local newCount = foundTokens + 4\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", newCount)\n else\n tokenManager.spawnTokenGroup(self, \"resource\", newCount)\n end\nend\n\nfunction takeAll(player)\n searchSelf()\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n playermatApi.updateCounter(matColor, \"ResourceCounter\", _, foundTokens)\n\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", 0)\n end\n printToColor(\"Moved \" .. foundTokens .. \" resource(s) to \" .. matColor .. \"'s resource pool.\", player.color)\nend\n\nfunction loseAll(player)\n searchSelf()\n if clickableResourceCounter then\n clickableResourceCounter.call(\"updateVal\", 0)\n end\n printToColor(\"Discarded \" .. foundTokens .. \" resource(s).\", player.color)\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -148236,7 +91954,7 @@ "scaleZ": 1 }, "Value": 0, - "XmlUI": "" + "XmlUI": "\u003c!-- include playercards/FamilyInheritance.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton padding=\"30 30 30 30\"\n font=\"font_teutonic-arkham\"\n textColor=\"white\"\n fontSize=\"235\"\n shadow=\"#405041B3\"\n shadowDistance=\"-15 15\"/\u003e\n \u003cTableLayout position=\"130 0 -22\"\n rotation=\"0 0 270\"\n height=\"460\"\n width=\"2600\"\n scale=\"0.1 0.1 1\"\n cellSpacing=\"80\"\n cellBackgroundColor=\"rgba(1,1,1,0)\"/\u003e\n\u003c/Defaults\u003e\n\n\u003cTableLayout id=\"Helper\"\n active=\"false\"\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"loseAll\"\n color=\"#6D202C\"\n fontSize=\"195\"\n text=\"Discard all\"/\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"takeAll\"\n color=\"#173B0B\"\n text=\"Move all\"/\u003e\n \u003c/Cell\u003e\n \u003cCell\u003e\n \u003cButton onClick=\"add4\"\n color=\"#77674D\"\n text=\"Place 4\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include playercards/FamilyInheritance.xml --\u003e" }, { "AltLookAngle": { @@ -148519,7 +92237,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/HeavyFurs\")\nend)\n__bundle_register(\"playercards/cards/HeavyFurs\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Skull\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Cultist\"] = true,\n [\"Frost\"] = true,\n [\"Custom Token\"] = true,\n [\"Elder Sign\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true\n}\n\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -149939,7 +93657,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ShortSupply\")\nend)\n__bundle_register(\"playercards/cards/ShortSupply\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Discard 10 cards\", shortSupply)\nend\n\n-- called by context menu entry\nfunction shortSupply(color)\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n\n -- get draw deck and discard position\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n local drawDeck = deckAreaObjects.draw\n local discardPos = playmatApi.getDiscardPosition(matColor)\n\n -- error handling\n if discardPos == nil then\n broadcastToColor(\"Couldn't retrieve discard position from playermat!\", color, \"Red\")\n return\n end\n\n if drawDeck == nil then\n broadcastToColor(\"Deck not found!\", color, \"Yellow\")\n return\n elseif drawDeck.type ~= \"Deck\" then\n broadcastToColor(\"Deck only contains a single card!\", color, \"Yellow\")\n return\n end\n\n -- discard cards, waiting 0.7 seconds between each discard to give players visiblity of the cards\n broadcastToColor(\"Discarding top 10 cards for player color '\" .. matColor .. \"'.\", color, \"White\")\n for i = 1, 10 do\n Wait.time(function() drawDeck.takeObject({ flip = true, position = { discardPos.x, 2 + 0.075 * i, discardPos.z } }) end, .7 * (i - 1))\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ShortSupply\")\nend)\n__bundle_register(\"playercards/cards/ShortSupply\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Discard 10 cards\", shortSupply)\nend\n\n-- called by context menu entry\nfunction shortSupply(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n\n -- get draw deck and discard position\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n local drawDeck = deckAreaObjects.draw\n local discardPos = playermatApi.getDiscardPosition(matColor)\n\n -- error handling\n if discardPos == nil then\n broadcastToColor(\"Couldn't retrieve discard position from playermat!\", playerColor, \"Red\")\n return\n end\n\n if drawDeck == nil then\n broadcastToColor(\"Deck not found!\", playerColor, \"Yellow\")\n return\n elseif drawDeck.type ~= \"Deck\" then\n broadcastToColor(\"Deck only contains a single card!\", playerColor, \"Yellow\")\n return\n end\n\n -- discard cards, waiting 0.7 seconds between each discard to give players visiblity of the cards\n broadcastToColor(\"Discarding top 10 cards for player color '\" .. matColor .. \"'.\", playerColor, \"White\")\n for i = 1, 10 do\n Wait.time(function() drawDeck.takeObject({ flip = true, position = { discardPos.x, 2 + 0.075 * i, discardPos.z } }) end, .7 * (i - 1))\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -150373,7 +94091,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/NkosiMabati3\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- XML background color for each token\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"\"] = \"#77674DE6\"\n}\n\nlocal sigil\n\nfunction onSave()\n return JSON.encode(sigil)\nend\n\nfunction onLoad(savedData)\n self.addContextMenuItem(\"Enable Helper\", chooseSigil)\n sigil = JSON.decode(savedData)\n if sigil and sigil ~= nil then\n makeXMLButton()\n self.clearContextMenu()\n self.addContextMenuItem(\"Clear Helper\", deleteButtons)\n end\nend\n\nfunction makeXMLButton()\n -- get name of the icon for the sigil (\"token\" + lowercase name without space characters)\n local sigilName = Global.call(\"getReadableTokenName\", sigil)\n local iconName = \"token-\" .. string.lower(sigilName)\n iconName = iconName:gsub(\"%s\", \"-\")\n\n self.UI.setXmlTable({\n {\n tag = \"Button\",\n attributes = {\n height = 450,\n width = 1400,\n rotation = \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = 300,\n iconWidth = \"400\",\n iconAlignment = \"Right\",\n onClick = \"resolveSigil\",\n id = sigil,\n icon = iconName,\n color = tokenColor[sigil],\n textColor = \"White\"\n },\n value = \"Resolve\"\n }\n }\n )\nend\n\n-- Create dialog window to choose sigil and create sigil-drawing button\nfunction chooseSigil(playerColor)\n self.clearContextMenu()\n self.addContextMenuItem(\"Clear Helper\", deleteButtons)\n\n -- get list of readable names\n local readableNames = {}\n for token, _ in pairs(tokenColor) do\n table.insert(readableNames, Global.call(\"getReadableTokenName\", token))\n end\n\n -- prompt player to choose sigil\n Player[playerColor].showOptionsDialog(\"Choose Sigil\", readableNames, 1,\n function(chosenToken)\n sigil = Global.call(\"getChaosTokenName\", chosenToken)\n makeXMLButton()\n end\n )\nend\n\n-- Delete button and remove sigil\nfunction deleteButtons()\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", chooseSigil)\n self.UI.setXml(\"\")\n sigil = nil\nend\n\nfunction resolveSigil()\n local tokensInPlay = chaosBagApi.getTokensInPlay()\n local chaosbag = chaosBagApi.findChaosBag()\n\n local match = false\n for _, obj in ipairs(chaosbag.getObjects()) do\n -- if there are any sigils in the bag\n if obj.nickname == sigil then\n match = true\n break\n end\n end\n\n if not match then\n broadcastToAll(Global.call(\"getReadableTokenName\", sigil) .. \" not found in chaos bag\", \"Red\")\n return\n end\n\n local matchingSymbolsInPlay = {}\n\n for _, token in ipairs(tokensInPlay) do\n if (token.getName() == \"Cultist\"\n or token.getName() == \"Tablet\"\n or token.getName() == \"Elder Thing\")\n and token.getName() ~= sigil then\n matchingSymbolsInPlay[#matchingSymbolsInPlay + 1] = token\n end\n end\n\n if #matchingSymbolsInPlay == 0 then\n broadcastToAll(\"No eligible symbol token found in play area\", \"Red\")\n return\n elseif #matchingSymbolsInPlay \u003e 1 then\n for _, token in ipairs(matchingSymbolsInPlay) do\n -- draw XML to return token to bag\n token.UI.setXmlTable({\n {\n tag = \"VerticalLayout\",\n attributes = {\n height = 275,\n width = 275,\n spacing = 0,\n padding = \"0 0 20 25\",\n scale = \"0.4 0.4 1\",\n rotation = \"0 0 180\",\n position = \"0 0 -15\",\n color = \"rgba(0,0,0,0.7)\",\n onClick = self.getGUID() .. \"/drawSigil(\" .. token.getGUID() .. \")\"\n },\n children = {\n {\n tag = \"Text\",\n attributes = {\n fontSize = \"100\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n text = \"Nkosi\"\n }\n },\n {\n tag = \"Text\",\n attributes = {\n fontSize = \"125\",\n font = \"font_arkhamicons\",\n color = \"#ffffff\",\n text = \"u\"\n }\n }\n }\n }\n })\n end\n else\n drawSigil(_, matchingSymbolsInPlay[1].getGUID())\n end\nend\n\nfunction drawSigil(player, tokenGUID)\n local returnedToken = getObjectFromGUID(tokenGUID)\n local matColor = playmatApi.getMatColorByPosition(returnedToken.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n chaosBagApi.drawChaosToken(mat, true, sigil, _, returnedToken)\n\n -- remove XML from tokens in play\n for _, token in ipairs(chaosBagApi.getTokensInPlay()) do\n if token.getName() == \"Cultist\"\n or token.getName() == \"Tablet\"\n or token.getName() == \"Elder Thing\" then\n token.UI.setXml(\"\")\n end\n end\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/NkosiMabati3\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/NkosiMabati3\")\nend)\n__bundle_register(\"playercards/cards/NkosiMabati3\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\n\n-- XML background color for each token\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave()\n return JSON.encode(sigil)\nend\n\nfunction onLoad(savedData)\n self.addContextMenuItem(\"Enable Helper\", chooseSigil)\n sigil = JSON.decode(savedData)\n if sigil and sigil ~= nil then\n makeXMLButton()\n self.clearContextMenu()\n self.addContextMenuItem(\"Clear Helper\", deleteButtons)\n end\nend\n\nfunction makeXMLButton()\n -- get name of the icon for the sigil (\"token\" + lowercase name without space characters)\n local sigilName = Global.call(\"getReadableTokenName\", sigil)\n local iconName = \"token-\" .. string.lower(sigilName)\n iconName = iconName:gsub(\"%s\", \"-\")\n\n self.UI.setXmlTable({\n {\n tag = \"Button\",\n attributes = {\n height = 450,\n width = 1400,\n rotation = \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = 300,\n iconWidth = \"400\",\n iconAlignment = \"Right\",\n onClick = \"resolveSigil\",\n icon = iconName,\n color = tokenColor[sigil],\n textColor = \"White\"\n },\n value = \"Resolve\"\n }\n }\n )\nend\n\n-- Create dialog window to choose sigil and create sigil-drawing button\nfunction chooseSigil(playerColor)\n Player[playerColor].clearSelectedObjects()\n self.clearContextMenu()\n self.addContextMenuItem(\"Clear Helper\", deleteButtons)\n\n -- get list of readable names\n local readableNames = {}\n for token, _ in pairs(tokenColor) do\n table.insert(readableNames, Global.call(\"getReadableTokenName\", token))\n end\n\n -- prompt player to choose sigil\n Player[playerColor].showOptionsDialog(\"Choose Sigil\", readableNames, 1,\n function(chosenToken)\n sigil = Global.call(\"getChaosTokenName\", chosenToken)\n makeXMLButton()\n end\n )\nend\n\n-- Delete button and remove sigil\nfunction deleteButtons(playerColor)\n Player[playerColor].clearSelectedObjects()\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", chooseSigil)\n self.UI.setXml(\"\")\n sigil = nil\nend\n\nfunction resolveSigil()\n local match = false\n for _, obj in ipairs(chaosBagApi.findChaosBag().getObjects()) do\n -- if there are any sigils in the bag\n if obj.nickname == sigil then\n match = true\n break\n end\n end\n\n if not match then\n broadcastToAll(Global.call(\"getReadableTokenName\", sigil) .. \" not found in chaos bag\", \"Red\")\n return\n end\n\n Global.call(\"activeRedrawEffect\", {\n DRAW_SPECIFIC_TOKEN = sigil,\n VALID_TOKENS = {\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Cultist\"] = true\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -156577,7 +100295,7 @@ }, "Description": "The Entertainer", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"05006\",\n \"alternate_ids\": [\n \"99001\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer. Sorcerer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 4,\n \"combatIcons\": 1,\n \"agilityIcons\": 3,\n \"cycle\": \"The Circle Undone\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05018\": 1\n },\n {\n \"05019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"spell\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"seeker\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Seeker and/or Survivor cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"05006\",\n \"alternate_ids\": [\n \"99001\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer. Sorcerer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 4,\n \"combatIcons\": 1,\n \"agilityIcons\": 3,\n \"cycle\": \"The Circle Undone\",\n \"extraToken\": \"Spell\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05018\": 1\n },\n {\n \"05019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"spell\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"seeker\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Seeker and/or Survivor cards\"\n }\n ]\n}", "GUID": "11122f", "Grid": true, "GridProjection": false, @@ -156639,7 +100357,7 @@ }, "Description": "The Fed", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01001-p\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n },\n {\n \"90025\": 1,\n \"90026\": 1,\n \"90027\": 1,\n \"90028\": 1,\n \"90029\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"insight\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01001-p\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"By the Book\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n },\n {\n \"90025\": 1,\n \"90026\": 1,\n \"90027\": 1,\n \"90028\": 1,\n \"90029\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"insight\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "502768", "Grid": true, "GridProjection": false, @@ -156701,7 +100419,7 @@ }, "Description": "The Ex-Con", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01003-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 25,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"gambit\",\n \"fortune\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01003-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"All or Nothing\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 25,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"gambit\",\n \"fortune\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "a03077", "Grid": true, "GridProjection": false, @@ -156763,7 +100481,7 @@ }, "Description": "The Athlete", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"05005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 5,\n \"cycle\": \"The Circle Undone\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05016\": 1\n },\n {\n \"05017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"trick\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"05005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 5,\n \"cycle\": \"The Circle Undone\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05016\": 1\n },\n {\n \"05017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"trick\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "bb8296", "Grid": true, "GridProjection": false, @@ -156887,7 +100605,7 @@ }, "Description": "The Bounty Hunter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"06003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal. Hunter.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 5,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dream-Eaters\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06010\": 1\n },\n {\n \"06011\": 2\n },\n {\n \"06012\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Guardian\",\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Seeker\",\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"06003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal. Hunter.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 5,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dream-Eaters\",\n \"extraToken\": \"Fight/Engage\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06010\": 1\n },\n {\n \"06011\": 2\n },\n {\n \"06012\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Guardian\",\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Seeker\",\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", "GUID": "53a412", "Grid": true, "GridProjection": false, @@ -156949,7 +100667,7 @@ }, "Description": "The Spy", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"07003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07010\": 1\n },\n {\n \"07011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"07003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07010\": 1\n },\n {\n \"07011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "333fe7", "Grid": true, "GridProjection": false, @@ -157073,7 +100791,7 @@ }, "Description": "The Priest", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"04004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Believer. Warden.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Forgotten Age\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04013\": 1\n },\n {\n \"04014\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"blessed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"04004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Believer. Warden.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Forgotten Age\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04013\": 1\n },\n {\n \"04014\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"blessed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "eb96e6", "Grid": true, "GridProjection": false, @@ -157135,7 +100853,7 @@ }, "Description": "The Salesman", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"08016\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Entrepreneur.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"Edge of the Earth\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08017\": 1\n },\n {\n \"08018\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Rogue cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"08016\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Entrepreneur.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"Edge of the Earth\",\n \"extraToken\": \"PlayItem\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08017\": 1\n },\n {\n \"08018\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Rogue cards\"\n }\n ]\n}", "GUID": "419b0c", "Grid": true, "GridProjection": false, @@ -157197,7 +100915,7 @@ }, "Description": "The Gravedigger", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03016\": 1\n },\n {\n \"03017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"03005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03016\": 1\n },\n {\n \"03017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "7e4c56", "Grid": true, "GridProjection": false, @@ -157259,7 +100977,7 @@ }, "Description": "The Haunted", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"04005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Cursed. Drifter.\",\n \"willpowerIcons\": 0,\n \"intellectIcons\": 0,\n \"combatIcons\": 0,\n \"agilityIcons\": 0,\n \"cycle\": \"The Forgotten Age\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04015\": 1\n },\n {\n \"04016\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"spirit\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"04005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Cursed. Drifter.\",\n \"willpowerIcons\": 0,\n \"intellectIcons\": 0,\n \"combatIcons\": 0,\n \"agilityIcons\": 0,\n \"cycle\": \"The Forgotten Age\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04015\": 1\n },\n {\n \"04016\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"spirit\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "b02a1e", "Grid": true, "GridProjection": false, @@ -157321,7 +101039,7 @@ }, "Description": "The Violinist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"06005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Performer. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dream-Eaters\",\n \"deck_requirements\": {\n \"size\": 42,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06016\": 1\n },\n {\n \"06017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"06005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Performer. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dream-Eaters\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 42,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06016\": 1\n },\n {\n \"06017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "a7b79f", "Grid": true, "GridProjection": false, @@ -157383,7 +101101,7 @@ }, "Description": "The Martial Artist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"08010\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Chosen. Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 3,\n \"cycle\": \"Edge of the Earth\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08011a\": 1,\n \"08012a\": 1,\n \"08013a\": 1,\n \"08014a\": 1\n },\n {\n \"08015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"firearm\"\n ]\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Guardian cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"08010\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Chosen. Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 3,\n \"cycle\": \"Edge of the Earth\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08011a\": 1,\n \"08012a\": 1,\n \"08013a\": 1,\n \"08014a\": 1\n },\n {\n \"08015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"firearm\"\n ]\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Guardian cards\"\n }\n ]\n}", "GUID": "cc21e0", "Grid": true, "GridProjection": false, @@ -157445,7 +101163,7 @@ }, "Description": "The Shaman", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03014\": 1\n },\n {\n \"03015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"uses\": [\n \"charge\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"03004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03014\": 1\n },\n {\n \"03015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"uses\": [\n \"charge\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n }\n ]\n}", "GUID": "452ed8", "Grid": true, "GridProjection": false, @@ -157507,7 +101225,7 @@ }, "Description": "The Student", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"07002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic. Scholar.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07008\": 1\n },\n {\n \"07009\": 1\n }\n ]\n \n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"practiced\"\n ],\n \"type\": [\n \"skill\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"07002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic. Scholar.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07008\": 1\n },\n {\n \"07009\": 1\n }\n ]\n \n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"practiced\"\n ],\n \"type\": [\n \"skill\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "05b950", "Grid": true, "GridProjection": false, @@ -157569,7 +101287,7 @@ }, "Description": "The Secretary", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Assistant.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Path to Carcosa\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03010\": 1\n },\n {\n \"03011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"03002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Assistant.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Path to Carcosa\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03010\": 1\n },\n {\n \"03011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "6c4c58", "Grid": true, "GridProjection": false, @@ -157631,7 +101349,7 @@ }, "Description": "The Researcher", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"06002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Assistant. Scholar.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 1,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dream-Eaters\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06008\": 1\n },\n {\n \"06009\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Rogue\",\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"06002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Assistant. Scholar.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 1,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dream-Eaters\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06008\": 1\n },\n {\n \"06009\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Rogue\",\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", "GUID": "57d586", "Grid": true, "GridProjection": false, @@ -157693,7 +101411,7 @@ }, "Description": "The Explorer", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"04002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"The Forgotten Age\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04008\": 1\n },\n {\n \"04009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"relic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"04002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Wayfarer.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"The Forgotten Age\",\n \"extraToken\": \"Investigate\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04008\": 1\n },\n {\n \"04009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"relic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n }\n ]\n}", "GUID": "07c37d", "Grid": true, "GridProjection": false, @@ -157755,7 +101473,7 @@ }, "Description": "The Letter Carrier", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"60501\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Chosen. Civic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"Investigator Packs\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60502\": 3\n },\n {\n \"60503\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"60501\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Chosen. Civic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"Investigator Packs\",\n \"extraToken\": \"Survivor\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60502\": 3\n },\n {\n \"60503\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "00e18e", "Grid": true, "GridProjection": false, @@ -157817,7 +101535,7 @@ }, "Description": "The Nun", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"07001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Blessed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07006\": 1\n },\n {\n \"07007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"07001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Blessed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07006\": 1\n },\n {\n \"07007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "617aeb", "Grid": true, "GridProjection": false, @@ -157879,7 +101597,7 @@ }, "Description": "The Drifter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Survivor or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02005\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90047\": 1,\n \"02014\": 1\n },\n {\n \"90048\": 1,\n \"02015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Survivor or Neutral\"\n }\n ]\n}", "GUID": "5294c3", "Grid": true, "GridProjection": false, @@ -157941,7 +101659,7 @@ }, "Description": "The Soldier", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Veteran.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 5,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03007\": 1\n },\n {\n \"03008\": 1\n },\n {\n \"03009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"03001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Veteran.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 5,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03007\": 1\n },\n {\n \"03008\": 1\n },\n {\n \"03009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n }\n ]\n}", "GUID": "01ac1b", "Grid": true, "GridProjection": false, @@ -158003,7 +101721,7 @@ }, "Description": "The Rookie Cop", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"06001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Police. Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dream-Eaters\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06006\": 1\n },\n {\n \"06007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"06001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Police. Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dream-Eaters\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06006\": 1\n },\n {\n \"06007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "e637cd", "Grid": true, "GridProjection": false, @@ -158065,7 +101783,7 @@ }, "Description": "The Mechanic", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"08001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Entrepreneur.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 1,\n \"combatIcons\": 5,\n \"agilityIcons\": 2,\n \"cycle\": \"Edge of the Earth\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08002\": 1\n },\n {\n \"08003\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Survivor cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"08001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Entrepreneur.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 1,\n \"combatIcons\": 5,\n \"agilityIcons\": 2,\n \"cycle\": \"Edge of the Earth\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"08002\": 1\n },\n {\n \"08003\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 level 0 Survivor cards\"\n }\n ]\n}", "GUID": "444830", "Grid": true, "GridProjection": false, @@ -158127,7 +101845,7 @@ }, "Description": "The Expedition Leader", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"04001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Veteran. Wayfarer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 1,\n \"cycle\": \"The Forgotten Age\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04006\": 1\n },\n {\n \"04007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"04001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Veteran. Wayfarer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 1,\n \"cycle\": \"The Forgotten Age\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"04006\": 1\n },\n {\n \"04007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "126932", "Grid": true, "GridProjection": false, @@ -158189,7 +101907,7 @@ }, "Description": "The Psychic", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"60401\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Clairvoyant.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 3,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Investigator Packs\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60402\": 1\n },\n {\n \"60403\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"60401\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Clairvoyant.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 3,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Investigator Packs\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60402\": 1\n },\n {\n \"60403\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "a2cd75", "Grid": true, "GridProjection": false, @@ -158251,7 +101969,7 @@ }, "Description": "The Ex-Con", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01003-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01003-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"All or Nothing\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "8116a6", "Grid": true, "GridProjection": false, @@ -158313,7 +102031,7 @@ }, "Description": "The Ex-Con", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01003-p\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 25,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"gambit\",\n \"fortune\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01003-p\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"All or Nothing\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 25,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"gambit\",\n \"fortune\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "22ebb2", "Grid": true, "GridProjection": false, @@ -158375,7 +102093,7 @@ }, "Description": "The Librarian", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01002-p\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"tome\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Guardian and/or Mystic cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01002-p\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Read or Die\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"tome\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Guardian and/or Mystic cards\"\n }\n ]\n}", "GUID": "282857", "Grid": true, "GridProjection": false, @@ -158437,7 +102155,7 @@ }, "Description": "The Librarian", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01002-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01002-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Read or Die\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "e8cafc", "Grid": true, "GridProjection": false, @@ -158499,7 +102217,7 @@ }, "Description": "The Librarian", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01002-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"tome\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Guardian and/or Mystic cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01002-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Read or Die\",\n \"extraToken\": \"Tome\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"trait\": [\n \"tome\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Guardian and/or Mystic cards\"\n }\n ]\n}", "GUID": "2f2e0d", "Grid": true, "GridProjection": false, @@ -158561,7 +102279,7 @@ }, "Description": "The Fed", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01001-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"insight\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01001-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"By the Book\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"insight\",\n \"tactic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "560cef", "Grid": true, "GridProjection": false, @@ -158623,7 +102341,7 @@ }, "Description": "The Fed", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01001-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n },\n {\n \"90025\": 1,\n \"90026\": 1,\n \"90027\": 1,\n \"90028\": 1,\n \"90029\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01001-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"By the Book\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n },\n {\n \"90025\": 1,\n \"90026\": 1,\n \"90027\": 1,\n \"90028\": 1,\n \"90029\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "f7361e", "Grid": true, "GridProjection": false, @@ -158685,7 +102403,7 @@ }, "Description": "The Aviatrix", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"60301\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 5,\n \"cycle\": \"Investigator Packs\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60302\": 1\n },\n {\n \"60303\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"60301\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 5,\n \"cycle\": \"Investigator Packs\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60302\": 1\n },\n {\n \"60303\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "cd4028", "Grid": true, "GridProjection": false, @@ -158747,7 +102465,7 @@ }, "Description": "The Professor", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"60201\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 5,\n \"combatIcons\": 1,\n \"agilityIcons\": 2,\n \"cycle\": \"Investigator Packs\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60202\": 1\n },\n {\n \"60203\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"60201\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 5,\n \"combatIcons\": 1,\n \"agilityIcons\": 2,\n \"cycle\": \"Investigator Packs\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60202\": 1\n },\n {\n \"60203\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "1fa944", "Grid": true, "GridProjection": false, @@ -158809,7 +102527,7 @@ }, "Description": "The Boxer", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"60101\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Criminal. Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 5,\n \"agilityIcons\": 2,\n \"cycle\": \"Investigator Packs\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60102\": 1\n },\n {\n \"60103\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"60101\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Criminal. Warden.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 5,\n \"agilityIcons\": 2,\n \"cycle\": \"Investigator Packs\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"60102\": 1\n },\n {\n \"60103\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "65588a", "Grid": true, "GridProjection": false, @@ -158871,7 +102589,7 @@ }, "Description": "The Millionaire", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"05003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Silver Twilight. Socialite.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"cycle\": \"The Circle Undone\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05011\": 1\n },\n {\n \"05012\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"illicit\"\n ]\n },\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"05003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Silver Twilight. Socialite.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"cycle\": \"The Circle Undone\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05011\": 1\n },\n {\n \"05012\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"illicit\"\n ]\n },\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "5e6298", "Grid": true, "GridProjection": false, @@ -158933,7 +102651,7 @@ }, "Description": "The Redeemed Cultist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"05004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Cultist. Silver Twilight.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Circle Undone\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05013\": 1\n },\n {\n \"05014\": 1\n },\n {\n \"05015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"05004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Cultist. Silver Twilight.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Circle Undone\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05013\": 1\n },\n {\n \"05014\": 1\n },\n {\n \"05015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "32b091", "Grid": true, "GridProjection": false, @@ -158995,7 +102713,7 @@ }, "Description": "The Chef", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Guardian or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Guardian or Neutral\"\n }\n ]\n}", "GUID": "98a0e1", "Grid": true, "GridProjection": false, @@ -159057,7 +102775,7 @@ }, "Description": "The Chef", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02001-p\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Path of the Righteous\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"trait\": [\n \"blessed\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Mystic cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02001-p\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Path of the Righteous\",\n \"extraToken\": \"FreeTrigger|Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"trait\": [\n \"blessed\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Mystic cards\"\n }\n ]\n}", "GUID": "98a0e2", "Grid": true, "GridProjection": false, @@ -159119,7 +102837,7 @@ }, "Description": "The Chef", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02001-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Path of the Righteous\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"trait\": [\n \"blessed\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Mystic cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02001-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Path of the Righteous\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"trait\": [\n \"blessed\",\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Mystic cards\"\n }\n ]\n}", "GUID": "98a0e4", "Grid": true, "GridProjection": false, @@ -159181,7 +102899,7 @@ }, "Description": "The Chef", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02001-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Path of the Righteous\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Guardian or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02001-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Believer. Hunter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Path of the Righteous\",\n \"extraToken\": \"FreeTrigger|Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02006\": 1,\n \"90060\": 1\n },\n {\n \"02007\": 1,\n \"90061\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Guardian or Neutral\"\n }\n ]\n}", "GUID": "98a0e3", "Grid": true, "GridProjection": false, @@ -159243,7 +102961,7 @@ }, "Description": "The Musician", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Mystic or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02004\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Mystic or Neutral\"\n }\n ]\n}", "GUID": "ca079b", "Grid": true, "GridProjection": false, @@ -159305,7 +103023,7 @@ }, "Description": "The Private Investigator", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"05002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Detective.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"The Circle Undone\",\n \"deck_requirements\": {\n \"size\": 40,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05009\": 1\n },\n {\n \"05010\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"05002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Detective.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"The Circle Undone\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 40,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05009\": 1\n },\n {\n \"05010\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "6dc626", "Grid": true, "GridProjection": false, @@ -159367,7 +103085,7 @@ }, "Description": "The Waitress", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01004-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"spell\",\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01004-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Bad Blood\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"spell\",\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "909f30", "Grid": true, "GridProjection": false, @@ -159429,7 +103147,7 @@ }, "Description": "The Actress", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03006\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03018\": 2\n },\n {\n \"03019\": 2\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"guardian\",\n \"seeker\",\n \"rogue\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n },\n \"error\": \"You must have at least 7 cards from 3 different factions\"\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"03006\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03018\": 2\n },\n {\n \"03019\": 2\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"guardian\",\n \"seeker\",\n \"rogue\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n },\n \"error\": \"You must have at least 7 cards from 3 different factions\"\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "d37332", "Grid": true, "GridProjection": false, @@ -159491,7 +103209,7 @@ }, "Description": "The Waitress", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01004-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01004-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Bad Blood\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "02db0a", "Grid": true, "GridProjection": false, @@ -159553,7 +103271,7 @@ }, "Description": "The Waitress", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01004-p\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"spell\",\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01004-p\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Bad Blood\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"spell\",\n \"occult\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n }\n ]\n}", "GUID": "01b6ef", "Grid": true, "GridProjection": false, @@ -162315,7 +106033,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n local matColor = playmatApi.getMatColor(playerColor)\n local deckPos = playmatApi.getDrawPosition(matColor)\n local deckRot = playmatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf(playerColor)\n Player[playerColor].clearSelectedObjects()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColor(playerColor)\n local deckPos = playermatApi.getDrawPosition(matColor)\n local deckRot = playermatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -162376,7 +106094,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n local matColor = playmatApi.getMatColor(playerColor)\n local deckPos = playmatApi.getDrawPosition(matColor)\n local deckRot = playmatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf(playerColor)\n Player[playerColor].clearSelectedObjects()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColor(playerColor)\n local deckPos = playermatApi.getDrawPosition(matColor)\n local deckRot = playermatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -162437,7 +106155,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n local matColor = playmatApi.getMatColor(playerColor)\n local deckPos = playmatApi.getDrawPosition(matColor)\n local deckRot = playmatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf(playerColor)\n Player[playerColor].clearSelectedObjects()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColor(playerColor)\n local deckPos = playermatApi.getDrawPosition(matColor)\n local deckRot = playermatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -162498,7 +106216,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n local matColor = playmatApi.getMatColor(playerColor)\n local deckPos = playmatApi.getDrawPosition(matColor)\n local deckRot = playmatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf(playerColor)\n Player[playerColor].clearSelectedObjects()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColor(playerColor)\n local deckPos = playermatApi.getDrawPosition(matColor)\n local deckRot = playermatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -162559,7 +106277,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n local matColor = playmatApi.getMatColor(playerColor)\n local deckPos = playmatApi.getDrawPosition(matColor)\n local deckRot = playmatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf(playerColor)\n Player[playerColor].clearSelectedObjects()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColor(playerColor)\n local deckPos = playermatApi.getDrawPosition(matColor)\n local deckRot = playermatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -162620,7 +106338,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n local matColor = playmatApi.getMatColor(playerColor)\n local deckPos = playmatApi.getDrawPosition(matColor)\n local deckRot = playmatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf(playerColor)\n Player[playerColor].clearSelectedObjects()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColor(playerColor)\n local deckPos = playermatApi.getDrawPosition(matColor)\n local deckRot = playermatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -162681,7 +106399,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n local matColor = playmatApi.getMatColor(playerColor)\n local deckPos = playmatApi.getDrawPosition(matColor)\n local deckRot = playmatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck\n ---@param obj tts__Object Object to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(obj, pos, rot, below)\n if obj == nil or pos == nil then return end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n local bounds = searchResult[1].getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- allow moving the objects smoothly out of the hand\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- continue if the card stops smooth moving\n Wait.condition(\n function()\n obj.use_hands = true\n -- this avoids a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and searchResult[1] ~= obj then\n -- call this with avoiding errors (physics is sometimes too fast so the object doesn't exist for the put)\n pcall(function() searchResult[1].putObject(obj) end)\n end\n end,\n function() return not obj.isSmoothMoving() end, 3)\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Tekeli-li\")\nend)\n__bundle_register(\"playercards/cards/Tekeli-li\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n self.addContextMenuItem(\"Return this card\", returnSelf)\n self.addContextMenuItem(\"Place below my deck\", placeBelowDeck)\nend\n\n-- uses the tekeli-li helper to place this card at the bottom of the deck\nfunction returnSelf(playerColor)\n Player[playerColor].clearSelectedObjects()\n local helper = getTekeliliHelper()\n if helper == nil then\n printToAll(\"Couldn't find Tekeli-li Helper!\")\n else\n helper.call(\"returnObject\", self)\n end\nend\n\n-- places this card below the deck of the player that triggered it\nfunction placeBelowDeck(playerColor)\n Player[playerColor].clearSelectedObjects()\n local matColor = playermatApi.getMatColor(playerColor)\n local deckPos = playermatApi.getDrawPosition(matColor)\n local deckRot = playermatApi.returnRotation(matColor)\n deckRot = deckRot:setAt(\"z\", 180)\n deckLib.placeOrMergeIntoDeck(self, Vector(deckPos), deckRot, true)\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -163234,7 +106952,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ClaypoolsFurs\")\nend)\n__bundle_register(\"playercards/cards/ClaypoolsFurs\", function(require, _LOADED, __bundle_register, __bundle_modules)\nbuttonLabel = \"Cancel\"\nbuttonIcon = \"token-frost\"\nbuttonColor = \"#404450E6\"\nbuttonFontSize = 300\n\nVALID_TOKENS = {\n [\"Frost\"] = true\n}\n\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -163597,7 +107315,7 @@ }, "Description": "The Urchin", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01005-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Blessed\",\n \"trait\": [\n \"blessed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Cursed\",\n \"trait\": [\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Both\",\n \"trait\": [\n \"blessed\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n },\n \"size\": 5\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01005-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Red Tide Rising\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Blessed\",\n \"trait\": [\n \"blessed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Cursed\",\n \"trait\": [\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Both\",\n \"trait\": [\n \"blessed\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n },\n \"size\": 5\n }\n ]\n}", "GUID": "4232d9", "Grid": true, "GridProjection": false, @@ -163606,7 +107324,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/WendyAdams\")\nend)\n__bundle_register(\"playercards/cards/WendyAdams\", function(require, _LOADED, __bundle_register, __bundle_modules)\nbuttonHeight = \"320\"\nbuttonWidth = \"1100\"\nbuttonPosition = \"70 -70 -22\"\nbuttonFontSize = 200\nbuttonRotation = \"0 0 90\"\n\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -163659,7 +107377,7 @@ }, "Description": "The Urchin", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01005-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter. Blessed. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n },\n {\n \"90038\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01005-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter. Blessed. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Red Tide Rising\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n },\n {\n \"90038\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "61503e", "Grid": true, "GridProjection": false, @@ -163721,7 +107439,7 @@ }, "Description": "The Urchin", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01005-p\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter. Blessed. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n },\n {\n \"90038\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Blessed\",\n \"trait\": [\n \"blessed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Cursed\",\n \"trait\": [\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Both\",\n \"trait\": [\n \"blessed\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n },\n \"size\": 5\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01005-p\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter. Blessed. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Red Tide Rising\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n },\n {\n \"90038\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Blessed\",\n \"trait\": [\n \"blessed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Cursed\",\n \"trait\": [\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Both\",\n \"trait\": [\n \"blessed\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n },\n \"size\": 5\n }\n ]\n}", "GUID": "fd91ea", "Grid": true, "GridProjection": false, @@ -163783,7 +107501,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90039\",\n \"type\": \"Asset\",\n \"slot\": \"Accessory\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Relic.\",\n \"willpowerIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90039\",\n \"type\": \"Asset\",\n \"slot\": \"Accessory\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Relic.\",\n \"willpowerIcons\": 1,\n \"wildIcons\": 2,\n \"cycle\": \"Red Ride Rising\"\n}", "GUID": "664b70", "Grid": true, "GridProjection": false, @@ -163845,7 +107563,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90040\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Madness.\",\n \"weakness\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90040\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Madness.\",\n \"weakness\": true,\n \"cycle\": \"Red Tide Rising\"\n}", "GUID": "89fe92", "Grid": true, "GridProjection": false, @@ -163967,7 +107685,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90038\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"Standalone\"\n}", + "GMNotes": "{\n \"id\": \"90038\",\n \"type\": \"Asset\",\n \"class\": \"Neutral\",\n \"startsInPlay\": true,\n \"permanent\": true,\n \"cycle\": \"Red Tide Rising\"\n}", "GUID": "b4f9ee", "Grid": true, "GridProjection": false, @@ -164038,7 +107756,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "do_not_ready = true", + "LuaScript": "", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -164048,6 +107766,7 @@ "Sticky": true, "Tags": [ "Asset", + "DoNotReady", "PlayerCard" ], "Tooltip": true, @@ -166865,7 +110584,7 @@ }, "Description": "The Louisiana Lion", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01048\",\n \"alternate_ids\": [\n \"01548\"\n ],\n \"type\": \"Asset\",\n \"slot\": \"Ally\",\n \"class\": \"Rogue\",\n \"cost\": 6,\n \"level\": 0,\n \"traits\": \"Ally. Criminal.\",\n \"intellectIcons\": 1,\n \"cycle\": \"Core\"\n}", + "GMNotes": "{\n \"id\": \"01048\",\n \"alternate_ids\": [\n \"01548\"\n ],\n \"type\": \"Asset\",\n \"slot\": \"Ally\",\n \"class\": \"Rogue\",\n \"cost\": 6,\n \"level\": 0,\n \"traits\": \"Ally. Criminal.\",\n \"intellectIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"Core\"\n}", "GUID": "eaa415", "Grid": true, "GridProjection": false, @@ -167171,7 +110890,7 @@ }, "Description": "The Louisiana Lion", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01054\",\n \"alternate_ids\": [\n \"01554\"\n ],\n \"type\": \"Asset\",\n \"slot\": \"Ally\",\n \"class\": \"Rogue\",\n \"cost\": 5,\n \"level\": 1,\n \"traits\": \"Ally. Criminal.\",\n \"intellectIcons\": 1,\n \"cycle\": \"Core\"\n}", + "GMNotes": "{\n \"id\": \"01054\",\n \"alternate_ids\": [\n \"01554\"\n ],\n \"type\": \"Asset\",\n \"slot\": \"Ally\",\n \"class\": \"Rogue\",\n \"cost\": 5,\n \"level\": 1,\n \"traits\": \"Ally. Criminal.\",\n \"intellectIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"Core\"\n}", "GUID": "27446e", "Grid": true, "GridProjection": false, @@ -170614,7 +114333,7 @@ }, "Description": "The Politician", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09018\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Civic. Socialite.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"cycle\": \"The Scarlet Keys\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09019\": 1\n },\n {\n \"09020\": 1\n }\n ],\n \"choices\": 2\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"ally\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Guardian\",\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Seeker\",\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Rogue\",\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"09018\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Civic. Socialite.\",\n \"willpowerIcons\": 1,\n \"intellectIcons\": 1,\n \"combatIcons\": 1,\n \"agilityIcons\": 1,\n \"cycle\": \"The Scarlet Keys\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09019\": 1\n },\n {\n \"09020\": 1\n }\n ],\n \"choices\": 2\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"ally\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Guardian\",\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Seeker\",\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Rogue\",\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "95fb5e", "Grid": true, "GridProjection": false, @@ -171043,7 +114762,7 @@ }, "Description": "The Doctor", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09004\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Medic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 1,\n \"cycle\": \"The Scarlet Keys\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09005\": 1\n },\n {\n \"09006\": 1\n },\n {\n \"09007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"special\": [\n \"heals_damage\"\n ],\n \"tag\": [\n \"hd\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"limit\": 15,\n \"error\": \"You cannot have more than 15 level 0-1 Guardian and/or Survivor cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"09004\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Medic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 3,\n \"agilityIcons\": 1,\n \"cycle\": \"The Scarlet Keys\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09005\": 1\n },\n {\n \"09006\": 1\n },\n {\n \"09007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"special\": [\n \"heals_damage\"\n ],\n \"tag\": [\n \"hd\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\",\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"limit\": 15,\n \"error\": \"You cannot have more than 15 level 0-1 Guardian and/or Survivor cards\"\n }\n ]\n}", "GUID": "c431f3", "Grid": true, "GridProjection": false, @@ -172034,7 +115753,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/CustomModifications\")\nend)\n__bundle_register(\"playercards/cards/CustomModifications\", function(require, _LOADED, __bundle_register, __bundle_modules)\nINVALID_TOKENS = {\n [\"Auto-fail\"] = true\n}\n\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -172332,7 +116051,7 @@ }, "Description": "The Photographer", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09015\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Scarlet Keys\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09016\": 1\n },\n {\n \"09017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"09015\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Scarlet Keys\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09016\": 1\n },\n {\n \"09017\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "5d3d67", "Grid": true, "GridProjection": false, @@ -172394,7 +116113,7 @@ }, "Description": "The Security Consultant", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09008\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"The Scarlet Keys\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09009\": 1\n },\n {\n \"09010\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\",\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"tool\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"09008\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 5,\n \"cycle\": \"The Scarlet Keys\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09009\": 1\n },\n {\n \"09010\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\",\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"tool\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n }\n ]\n}", "GUID": "9a9830", "Grid": true, "GridProjection": false, @@ -172456,7 +116175,7 @@ }, "Description": "The Butler", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Assistant.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Scarlet Keys\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09002\": 2\n },\n {\n \"09003\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Seeker\",\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"09001\",\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Assistant.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Scarlet Keys\",\n \"extraToken\": \"Activate\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09002\": 2\n },\n {\n \"09003\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Seeker\",\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", "GUID": "dc96d1", "Grid": true, "GridProjection": false, @@ -173989,7 +117708,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09104\",\n \"type\": \"Event\",\n \"class\": \"Survivor\",\n \"cost\": 0,\n \"level\": 0,\n \"traits\": \"Insight. Spirit.\",\n \"cycle\": \"The Scarlet Keys\"\n}", + "GMNotes": "{\n \"id\": \"09104\",\n \"type\": \"Event\",\n \"class\": \"Survivor\",\n \"cost\": 0,\n \"level\": 0,\n \"traits\": \"Insight. Spirit.\",\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Scarlet Keys\"\n}", "GUID": "a3d041", "Grid": true, "GridProjection": false, @@ -174790,7 +118509,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09091\",\n \"type\": \"Asset\",\n \"slot\": \"Body\",\n \"class\": \"Mystic\",\n \"cost\": 3,\n \"level\": 2,\n \"traits\": \"Ritual.\",\n \"combatIcons\": 1,\n \"cycle\": \"The Scarlet Keys\"\n}", + "GMNotes": "{\n \"id\": \"09091\",\n \"type\": \"Asset\",\n \"slot\": \"Body\",\n \"class\": \"Mystic\",\n \"cost\": 3,\n \"level\": 2,\n \"traits\": \"Ritual.\",\n \"combatIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"PlayItem\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Scarlet Keys\"\n}", "GUID": "b5d894", "Grid": true, "GridProjection": false, @@ -175467,7 +119186,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09080\",\n \"type\": \"Asset\",\n \"slot\": \"Ally|Arcane\",\n \"class\": \"Mystic\",\n \"cost\": 2,\n \"level\": 0,\n \"traits\": \"Summon.\",\n \"willpowerIcons\": 1,\n \"customizations\": [\n {\n \"name\": \"Armored Carapace\",\n \"xp\": 1,\n \"text\": \"Summoned Servitor gains a health value of 3. It can be assigned damage dealt to any investigator at its location.\"\n },\n {\n \"name\": \"Claws that Catch\",\n \"xp\": 1,\n \"text\": \"Add this action: “⟐ Fight. You fight any enemy at this location with a base Combat of 4. Ignore the aloof and retaliate keywords for this attack.”\"\n },\n {\n \"name\": \"Jaws that Snatch\",\n \"xp\": 1,\n \"text\": \"Add this action: “⟐ Evade. You attempt to evade any enemy at this location with a base Agility of 4. Ignore the alert keyword for this evasion attempt.”\"\n },\n {\n \"name\": \"Eyes of Flame\",\n \"xp\": 1,\n \"text\": \"Add this action: “⟐ Investigate. You investigate this location with a base Intellect of 4.”\"\n },\n {\n \"name\": \"Wings of Night\",\n \"xp\": 1,\n \"text\": \"After Summoned Servitor moves from your location to a connecting location, you may move to that location, as well.\"\n },\n {\n \"name\": \"Dominance\",\n \"xp\": 2,\n \"text\": \"Summoned Servitor no longer takes up an (select one): arcane / ally slot.\"\n },\n {\n \"name\": \"Dreaming Call\",\n \"xp\": 3,\n \"text\": \"Instead of discarding another asset you control in order to play Summoned Servitor, you may return that asset to its owner’s hand.\"\n },\n {\n \"name\": \"Dæmonic Influence\",\n \"xp\": 5,\n \"text\": \"Summoned Servitor can take 2 different actions instead of 1 during each of your turns.\"\n }\n ],\n \"cycle\": \"The Scarlet Keys\"\n}", + "GMNotes": "{\n \"id\": \"09080\",\n \"type\": \"Asset\",\n \"slot\": \"Ally|Arcane\",\n \"class\": \"Mystic\",\n \"cost\": 2,\n \"level\": 0,\n \"traits\": \"Summon.\",\n \"willpowerIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"customizations\": [\n {\n \"name\": \"Armored Carapace\",\n \"xp\": 1,\n \"text\": \"Summoned Servitor gains a health value of 3. It can be assigned damage dealt to any investigator at its location.\"\n },\n {\n \"name\": \"Claws that Catch\",\n \"xp\": 1,\n \"text\": \"Add this action: “⟐ Fight. You fight any enemy at this location with a base Combat of 4. Ignore the aloof and retaliate keywords for this attack.”\"\n },\n {\n \"name\": \"Jaws that Snatch\",\n \"xp\": 1,\n \"text\": \"Add this action: “⟐ Evade. You attempt to evade any enemy at this location with a base Agility of 4. Ignore the alert keyword for this evasion attempt.”\"\n },\n {\n \"name\": \"Eyes of Flame\",\n \"xp\": 1,\n \"text\": \"Add this action: “⟐ Investigate. You investigate this location with a base Intellect of 4.”\"\n },\n {\n \"name\": \"Wings of Night\",\n \"xp\": 1,\n \"text\": \"After Summoned Servitor moves from your location to a connecting location, you may move to that location, as well.\"\n },\n {\n \"name\": \"Dominance\",\n \"xp\": 2,\n \"text\": \"Summoned Servitor no longer takes up an (select one): arcane / ally slot.\"\n },\n {\n \"name\": \"Dreaming Call\",\n \"xp\": 3,\n \"text\": \"Instead of discarding another asset you control in order to play Summoned Servitor, you may return that asset to its owner’s hand.\"\n },\n {\n \"name\": \"Dæmonic Influence\",\n \"xp\": 5,\n \"text\": \"Summoned Servitor can take 2 different actions instead of 1 during each of your turns.\",\n \"replaces\": {\n \"uses\": [\n {\n \"count\": 2,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ]\n }\n }\n ],\n \"cycle\": \"The Scarlet Keys\"\n}", "GUID": "73b311", "Grid": true, "GridProjection": false, @@ -176637,7 +120356,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09061\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 1,\n \"level\": 0,\n \"traits\": \"Gambit.\",\n \"agilityIcons\": 1,\n \"customizations\": [\n {\n \"name\": \"Reflex Response\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- You take damage or horror.”\"\n },\n {\n \"name\": \"Situational Awareness\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- A location enters play or is revealed.”\"\n },\n {\n \"name\": \"Killer Instinct\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- An enemy engages you.”\"\n },\n {\n \"name\": \"Gut Reaction\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- A treachery enters your threat area .”\"\n },\n {\n \"name\": \"Muscle Memory\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- You play an asset.”\"\n },\n {\n \"name\": \"Sharpened Talent\",\n \"xp\": 2,\n \"text\": \"During the action granted by Honed Instinct, you get +2 to each of your skills.\"\n },\n {\n \"name\": \"Impulse Control\",\n \"xp\": 3,\n \"text\": \"You may include up to three copies of Honed Instinct in your deck. Honed Instinct gets –1 cost.\",\n \"replaces\": {\n \"cost\": 0\n }\n },\n {\n \"name\": \"Force of Habit\",\n \"xp\": 5,\n \"text\": \"When you play Honed Instinct, you may take 2 actions instead of 1 (one at a time). Then, remove it from the game.\"\n }\n ],\n \"cycle\": \"The Scarlet Keys\"\n}", + "GMNotes": "{\n \"id\": \"09061\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 1,\n \"level\": 0,\n \"traits\": \"Gambit.\",\n \"agilityIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"customizations\": [\n {\n \"name\": \"Reflex Response\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- You take damage or horror.”\"\n },\n {\n \"name\": \"Situational Awareness\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- A location enters play or is revealed.”\"\n },\n {\n \"name\": \"Killer Instinct\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- An enemy engages you.”\"\n },\n {\n \"name\": \"Gut Reaction\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- A treachery enters your threat area .”\"\n },\n {\n \"name\": \"Muscle Memory\",\n \"xp\": 1,\n \"text\": \"Add the following play condition: “- You play an asset.”\"\n },\n {\n \"name\": \"Sharpened Talent\",\n \"xp\": 2,\n \"text\": \"During the action granted by Honed Instinct, you get +2 to each of your skills.\"\n },\n {\n \"name\": \"Impulse Control\",\n \"xp\": 3,\n \"text\": \"You may include up to three copies of Honed Instinct in your deck. Honed Instinct gets –1 cost.\",\n \"replaces\": {\n \"cost\": 0\n }\n },\n {\n \"name\": \"Force of Habit\",\n \"xp\": 5,\n \"text\": \"When you play Honed Instinct, you may take 2 actions instead of 1 (one at a time). Then, remove it from the game.\",\n \"replaces\": {\n \"uses\": [\n {\n \"count\": 2,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ]\n }\n }\n ],\n \"cycle\": \"The Scarlet Keys\"\n}", "GUID": "1cde62", "Grid": true, "GridProjection": false, @@ -177131,7 +120850,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09053\",\n \"type\": \"Event\",\n \"class\": \"Seeker\",\n \"cost\": 0,\n \"level\": 1,\n \"traits\": \"Insight.\",\n \"willpowerIcons\": 1,\n \"wildIcons\": 1,\n \"cycle\": \"The Scarlet Keys\"\n}", + "GMNotes": "{\n \"id\": \"09053\",\n \"type\": \"Event\",\n \"class\": \"Seeker\",\n \"cost\": 0,\n \"level\": 1,\n \"traits\": \"Insight.\",\n \"willpowerIcons\": 1,\n \"wildIcons\": 1,\n \"uses\": [\n {\n \"count\": 1,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Scarlet Keys\"\n}", "GUID": "425841", "Grid": true, "GridProjection": false, @@ -177386,7 +121105,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/Analysis\")\nend)\n__bundle_register(\"playercards/cards/Analysis\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -177877,7 +121596,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/EmpiricalHypothesis\")\nend)\n__bundle_register(\"playercards/cards/EmpiricalHypothesis\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this helper creates buttons to help the user track which hypothesis has been chosen each round\n-- (if user forgot to choose one at round start, the old one stays active)\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal upgradeSheetLibrary = require(\"playercards/customizable/UpgradeSheetLibrary\")\n\n-- common button parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.height = 160\nbuttonParameters.width = 1000\nbuttonParameters.font_size = 84\nbuttonParameters.font_color = { 1.0, 1.0, 1.0 }\nbuttonParameters.color = Color.Black\nbuttonParameters.position = {}\nbuttonParameters.position.x = 0\nbuttonParameters.position.y = 0.6\nbuttonParameters.position.z = -1.05\ninitialButtonPosition = buttonParameters.position.z\n\n-- vertical offset between buttons\nlocal verticalOffset = 0.325\n\n-- list of customizable labels\nlocal customizableList = {\n 'Run out of cards in hand',\n 'Take damage/horror',\n 'Discard treachery/enemy',\n 'Enter 3 or more shroud'\n}\n\n-- index of the currently selected button (0-indexed from the top)\nlocal activeButtonIndex\n\nfunction onSave()\n return JSON.encode(activeButtonIndex)\nend\n\nfunction onLoad(savedData)\n self.addContextMenuItem(\"Enable Helper\", createButtons)\n self.addContextMenuItem(\"Clear Helper\", deleteButtons)\n\n activeButtonIndex = JSON.decode(savedData)\n if activeButtonIndex and activeButtonIndex ~= \"\" then\n local tempButtonIndex = activeButtonIndex\n createButtons()\n if tempButtonIndex \u003e= 0 then\n selectButton(tempButtonIndex)\n end\n end\nend\n\n-- marks a button as active\n---@param index number Index of the button to mark (starts at 0 from the top)\nfunction selectButton(index)\n local lastindex = #hypothesisList - 1\n for i = 0, lastindex do\n local color = Color.Black\n if i == index then\n color = Color.Red\n activeButtonIndex = i\n end\n self.editButton({ index = i, color = color })\n end\nend\n\nfunction deleteButtons()\n self.clearButtons()\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", createButtons)\n buttonParameters.position.z = initialButtonPosition -- reset the z position\nend\n\n-- Create buttons based on the button parameters\nfunction createButtons()\n self.clearContextMenu()\n self.addContextMenuItem(\"Clear Helper\", deleteButtons)\n\n -- reset the list in case of addition of checkboxes or Refine\n hypothesisList = {\n 'Succeed by 3 or more',\n 'Fail by 2 or more'\n }\n\n -- set activeButtonIndex to restore state onLoad (\"-1\" -\u003e nothing selected)\n activeButtonIndex = -1\n\n -- get the upgradesheet and check for more conditions\n local upgradeSheet = findUpgradeSheet()\n if upgradeSheet then\n for i = 1, 4 do\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n table.insert(hypothesisList, customizableList[i])\n end\n end\n end\n\n for i, label in ipairs(hypothesisList) do\n buttonParameters.click_function = \"selectButton\" .. i\n self.setVar(buttonParameters.click_function, function() selectButton(i - 1) end)\n buttonParameters.label = label\n self.createButton(buttonParameters)\n buttonParameters.position.z = buttonParameters.position.z + verticalOffset\n end\nend\n\nfunction findUpgradeSheet()\n local matColor = playmatApi.getMatColorByPosition(self.getPosition())\n local result = playmatApi.searchAroundPlaymat(matColor, \"isCard\")\n for j, card in ipairs(result) do\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.id == \"09041-c\" then\n return card\n end\n end\nend\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/EmpiricalHypothesis\")\nend)\n__bundle_register(\"playercards/cards/EmpiricalHypothesis\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardsWithHelper\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- intentionally global\nhasXML = false\nisHelperEnabled = false\n\n-- common button parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.height = 160\nbuttonParameters.width = 1000\nbuttonParameters.font_size = 84\nbuttonParameters.font_color = { 1.0, 1.0, 1.0 }\nbuttonParameters.color = Color.Black\nbuttonParameters.position = {}\nbuttonParameters.position.x = 0\nbuttonParameters.position.y = 0.6\nbuttonParameters.position.z = -1.05\ninitialButtonPosition = buttonParameters.position.z\n\n-- vertical offset between buttons\nlocal verticalOffset = 0.325\n\n-- list of customizable labels\nlocal customizableList = {\n 'Run out of cards in hand',\n 'Take damage/horror',\n 'Discard treachery/enemy',\n 'Enter 3 or more shroud'\n}\n\n-- index of the currently selected button (0-indexed from the top)\nlocal activeButtonIndex = -1\nlocal hypothesisList = {}\n\nfunction updateSave()\n self.script_state = JSON.encode({\n isHelperEnabled = isHelperEnabled,\n activeButtonIndex = activeButtonIndex\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n activeButtonIndex = loadedData.activeButtonIndex\n end\n\n if activeButtonIndex \u003e 0 then\n selectButton(activeButtonIndex)\n end\n\n syncDisplayWithOptionPanel()\nend\n\nfunction initialize()\n createButtons()\nend\n\nfunction shutOff()\n self.clearButtons()\nend\n\n-- marks a button as active\n---@param index number Index of the button to mark (starts at 0 from the top)\nfunction selectButton(index)\n local lastindex = #hypothesisList - 1\n for i = 0, lastindex do\n local buttonColor = Color.Black\n if i == index then\n buttonColor = Color.Red\n activeButtonIndex = i\n end\n self.editButton({ index = i, color = buttonColor })\n end\nend\n\n-- create buttons based on the button parameters\nfunction createButtons()\n self.clearButtons()\n\n -- reset the list in case of addition of checkboxes or Refine\n hypothesisList = { 'Succeed by 3 or more', 'Fail by 2 or more' }\n\n -- set activeButtonIndex to restore state onLoad (\"-1\" -\u003e nothing selected)\n activeButtonIndex = -1\n\n -- get the upgradesheet and check for more conditions\n local upgradeSheet = findUpgradeSheet()\n if upgradeSheet then\n for i = 1, 4 do\n if upgradeSheet.call(\"isUpgradeActive\", i) then\n table.insert(hypothesisList, customizableList[i])\n end\n end\n end\n\n -- reset the z position\n buttonParameters.position.z = initialButtonPosition\n\n -- actual button creation\n for i, label in ipairs(hypothesisList) do\n buttonParameters.label = label\n buttonParameters.click_function = \"selectButton\" .. i\n self.setVar(buttonParameters.click_function, function() selectButton(i - 1) end)\n self.createButton(buttonParameters)\n buttonParameters.position.z = buttonParameters.position.z + verticalOffset\n end\nend\n\nfunction findUpgradeSheet()\n local matColor = playermatApi.getMatColorByPosition(self.getPosition())\n local result = playermatApi.searchAroundPlayermat(matColor, \"isCard\")\n for j, card in ipairs(result) do\n local metadata = JSON.decode(card.getGMNotes()) or {}\n if metadata.id == \"09041-c\" then\n return card\n end\n end\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -178922,7 +122641,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/SummonedServitorUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/SummonedServitorUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Summoned Servitor\n\n-- Color information for buttons\nboxSize = 35\n\n-- static values\nxInitial = -0.935\nxOffset = 0.068\n\n-- Locations of the slot selectors\nSLOT_ICON_POSITIONS = {\n arcane = { x = 0.160, z = 0.65 },\n ally = { x = -0.073, z = 0.65 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nSLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n--selectedSlot = SLOT_INDICES.none\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.92,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.625,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.33,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.055,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.26,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.56,\n count = 2,\n }\n -- Row 6 includes the selection of Arcane/Ally slot, presented with buttons but stored\n -- as a text field\n },\n [7] = {\n checkboxes = {\n posZ = 0.765,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 1.06,\n count = 5,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/SummonedServitorUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/SummonedServitorUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Summoned Servitor\n\n-- Color information for buttons\nboxSize = 35\n\n-- static values\nxInitial = -0.935\nxOffset = 0.068\n\n-- Locations of the slot selectors\nSLOT_ICON_POSITIONS = {\n arcane = { x = 0.160, z = 0.65 },\n ally = { x = -0.073, z = 0.65 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nSLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n--selectedSlot = SLOT_INDICES.none\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.92,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.625,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.33,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.055,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.26,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.56,\n count = 2,\n }\n -- Row 6 includes the selection of Arcane/Ally slot, presented with buttons but stored\n -- as a text field\n },\n [7] = {\n checkboxes = {\n posZ = 0.765,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 1.06,\n count = 5,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -178983,7 +122702,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/RunicAxeUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/RunicAxeUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Runic Axe\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0705\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.92,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.715,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.415,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.018,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.265,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.66,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.86,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 1.065,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/RunicAxeUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/RunicAxeUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Runic Axe\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0705\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.92,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.715,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.415,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.018,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.265,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.66,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.86,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 1.065,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179044,7 +122763,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/PowerWordUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/PowerWordUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Power Word\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.933\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.6,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.32,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.02,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.28,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.48,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.775,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.975,\n count = 3,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/customizable/PowerWordUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Power Word\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.933\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.6,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.32,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.02,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.28,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.48,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.775,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.975,\n count = 3,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/PowerWordUpgradeSheet\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179105,7 +122824,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/PocketMultiToolUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/PocketMultiToolUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Pocket Multi Tool\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.892,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.560,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.326,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.092,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.142,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.376,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.610,\n count = 4,\n },\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/PocketMultiToolUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/PocketMultiToolUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Pocket Multi Tool\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.892,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.560,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.326,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.092,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.142,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.376,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.610,\n count = 4,\n },\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179166,7 +122885,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/MakeshiftTrapUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/MakeshiftTrapUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Makeshift Trap\n\n-- Color information for buttons\nboxSize = 39\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0735\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.889,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.655,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.325,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.085,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.252,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.585,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.927,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/MakeshiftTrapUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/MakeshiftTrapUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Makeshift Trap\n\n-- Color information for buttons\nboxSize = 39\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0735\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.889,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.655,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.325,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.085,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.252,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.585,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.927,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179227,7 +122946,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/LivingInkUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/LivingInkUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Living Ink\n\n-- Size information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.935\nxOffset = 0.075\n\n-- Locations of the skill selectors\nSKILL_ICON_POSITIONS = {\n willpower = { x = 0.085, z = -0.88 },\n intellect = { x = -0.183, z = -0.88 },\n combat = { x = -0.473, z = -0.88 },\n agility = { x = -0.74, z = -0.88 },\n}\n\ncustomizations = {\n [1] = { }, -- Empty placeholder for skill selection row, handled by custom skill display\n [2] = {\n checkboxes = {\n posZ = -0.69,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.355,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.0855,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.425,\n count = 2,\n }\n },\n [6] = {\n checkboxes = {\n posZ = 0.555,\n count = 3,\n },\n },\n [7] = {\n checkboxes = {\n posZ = 0.685,\n count = 3,\n }\n },\n [8] = {\n checkboxes = {\n posZ = 1.02,\n count = 3,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/LivingInkUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/LivingInkUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Living Ink\n\n-- Size information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.935\nxOffset = 0.075\n\n-- Locations of the skill selectors\nSKILL_ICON_POSITIONS = {\n willpower = { x = 0.085, z = -0.88 },\n intellect = { x = -0.183, z = -0.88 },\n combat = { x = -0.473, z = -0.88 },\n agility = { x = -0.74, z = -0.88 },\n}\n\ncustomizations = {\n [1] = { }, -- Empty placeholder for skill selection row, handled by custom skill display\n [2] = {\n checkboxes = {\n posZ = -0.69,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.355,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.0855,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.425,\n count = 2,\n }\n },\n [6] = {\n checkboxes = {\n posZ = 0.555,\n count = 3,\n },\n },\n [7] = {\n checkboxes = {\n posZ = 0.685,\n count = 3,\n }\n },\n [8] = {\n checkboxes = {\n posZ = 1.02,\n count = 3,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179288,7 +123007,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/HyperphysicalShotcasterUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/HyperphysicalShotcasterUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Hyperphysical Shotcaster\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.9,\n count = 2,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.615,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.237,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.232,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.61,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.988,\n count = 4,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 1.185,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/customizable/HyperphysicalShotcasterUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Hyperphysical Shotcaster\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.9,\n count = 2,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.615,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.237,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.232,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.61,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.988,\n count = 4,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 1.185,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/HyperphysicalShotcasterUpgradeSheet\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179349,7 +123068,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/HuntersArmorUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/HuntersArmorUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Hunter's Armor\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.892,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.560,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.220,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.092,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.047,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.376,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.820,\n count = 3,\n },\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/HuntersArmorUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/HuntersArmorUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Hunter's Armor\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.892,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.560,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.220,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.092,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.047,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.376,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.820,\n count = 3,\n },\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179410,7 +123129,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/customizable/HonedInstinctUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Honed Instinct\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.705,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.5,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.29,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = -0.09,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.12,\n count = 2,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.325,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.62,\n count = 5,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/HonedInstinctUpgradeSheet\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/HonedInstinctUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/HonedInstinctUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Honed Instinct\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.705,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.5,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.29,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = -0.09,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.12,\n count = 2,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.325,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.62,\n count = 5,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179471,7 +123190,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/GrizzledUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/GrizzledUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Grizzled\n\n-- Color information for buttons and input boxes\nboxSize = 40\ninputFontsize = 50\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n textField = {\n position = { 0.3, 0.25, -0.91 },\n width = 600\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.71,\n count = 1,\n },\n textField = {\n position = { 0.005, 0.25, -0.58 },\n width = 875\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.458,\n count = 2,\n },\n textField = {\n position = { 0.005, 0.25, -0.32 },\n width = 875\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.205,\n count = 3,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.362,\n count = 4,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.82,\n count = 5,\n },\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/GrizzledUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/GrizzledUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Grizzled\n\n-- Color information for buttons and input boxes\nboxSize = 40\ninputFontsize = 50\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n textField = {\n position = { 0.3, 0.25, -0.91 },\n width = 600\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.71,\n count = 1,\n },\n textField = {\n position = { 0.005, 0.25, -0.58 },\n width = 875\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.458,\n count = 2,\n },\n textField = {\n position = { 0.005, 0.25, -0.32 },\n width = 875\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.205,\n count = 3,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.362,\n count = 4,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.82,\n count = 5,\n },\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179532,7 +123251,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/FriendsinLowPlacesUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/FriendsinLowPlacesUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Friends in Low Places\n\n-- Color information for buttons and input boxes\nboxSize = 36\ninputFontsize = 50\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0685\n\ncustomizations = {\n [1] = {\n textField = {\n position = { 0.275, 0.25, -0.91 },\n width = 640\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.725,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.44,\n count = 2,\n },\n textField = {\n position = { 0.6295, 0.25, -0.44 },\n width = 290\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.05,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.25,\n count = 2,\n }\n },\n [6] = {\n checkboxes = {\n posZ = 0.545,\n count = 2,\n },\n },\n [7] = {\n checkboxes = {\n posZ = 0.75,\n count = 3,\n }\n },\n [8] = {\n checkboxes = {\n posZ = 0.95,\n count = 3,\n }\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/FriendsinLowPlacesUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/FriendsinLowPlacesUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Friends in Low Places\n\n-- Color information for buttons and input boxes\nboxSize = 36\ninputFontsize = 50\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0685\n\ncustomizations = {\n [1] = {\n textField = {\n position = { 0.275, 0.25, -0.91 },\n width = 640\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.725,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.44,\n count = 2,\n },\n textField = {\n position = { 0.6295, 0.25, -0.44 },\n width = 290\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.05,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.25,\n count = 2,\n }\n },\n [6] = {\n checkboxes = {\n posZ = 0.545,\n count = 2,\n },\n },\n [7] = {\n checkboxes = {\n posZ = 0.75,\n count = 3,\n }\n },\n [8] = {\n checkboxes = {\n posZ = 0.95,\n count = 3,\n }\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179593,7 +123312,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/EmpiricalHypothesisUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/EmpiricalHypothesisUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Empirical Hypothesis\n\n-- Color information for buttons\nboxSize = 37\n\n-- static values\nxInitial = -0.935\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.7,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.505,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.3,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = -0.09,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.3,\n count = 2,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.592,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.888,\n count = 4,\n }\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/EmpiricalHypothesisUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/EmpiricalHypothesisUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Empirical Hypothesis\n\n-- Color information for buttons\nboxSize = 37\n\n-- static values\nxInitial = -0.935\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.7,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.505,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.3,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = -0.09,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.3,\n count = 2,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.592,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.888,\n count = 4,\n }\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179654,7 +123373,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/customizable/DamningTestimonyUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Damning Testimony\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.935\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.925,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.475,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.25,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.01,\n count = 3,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.428,\n count = 3,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.772,\n count = 4,\n }\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/DamningTestimonyUpgradeSheet\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/DamningTestimonyUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/DamningTestimonyUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Damning Testimony\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.935\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.925,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.475,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.25,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.01,\n count = 3,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.428,\n count = 3,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.772,\n count = 4,\n }\n },\n}\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179715,7 +123434,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/CustomModificationsUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/CustomModificationsUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Custom Modifications\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0735\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.895,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.455,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.215,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.115,\n count = 3,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.453,\n count = 3,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.794,\n count = 4,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/CustomModificationsUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/CustomModificationsUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Custom Modifications\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0735\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.895,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.455,\n count = 2,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.215,\n count = 2,\n }\n },\n [4] = {\n checkboxes = {\n posZ = 0.115,\n count = 3,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.453,\n count = 3,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.794,\n count = 4,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179776,7 +123495,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/AlchemicalDistillationUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/AlchemicalDistillationUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Alchemical Distillation\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.892,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.665,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.43,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.092,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.142,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.376,\n count = 4,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.815,\n count = 5,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/AlchemicalDistillationUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/AlchemicalDistillationUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Alchemical Distillation\n\n-- Color information for buttons\nboxSize = 40\n\n-- static values\nxInitial = -0.933\nxOffset = 0.075\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.892,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.665,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.43,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.092,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.142,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.376,\n count = 4,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.815,\n count = 5,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -179828,7 +123547,7 @@ }, "Description": "The Painter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Artist.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Path to Carcosa\",\n \"deck_requirements\": {\n \"size\": 33,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03012\": 3\n },\n {\n \"03013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"03003\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Artist.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Path to Carcosa\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 33,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03012\": 3\n },\n {\n \"03013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "342311", "Grid": true, "GridProjection": false, @@ -180014,7 +123733,7 @@ }, "Description": "The Reporter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02008\": 1\n },\n {\n \"02009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"fortune\"\n ]\n },\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Seeker or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02002\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02008\": 1\n },\n {\n \"02009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"fortune\"\n ]\n },\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Seeker or Neutral\"\n }\n ]\n}", "GUID": "4271cb", "Grid": true, "GridProjection": false, @@ -180322,7 +124041,7 @@ }, "Description": "The Operator", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"09011\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Chosen. Cursed.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Scarlet Keys\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09012\": 1,\n \"09013\": 1\n },\n {\n \"09014\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"09011\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Chosen. Cursed.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Scarlet Keys\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"09012\": 1,\n \"09013\": 1\n },\n {\n \"09014\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"charm\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n }\n ]\n}", "GUID": "4c2a3d", "Grid": true, "GridProjection": false, @@ -180393,7 +124112,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/TheRavenQuillUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/TheRavenQuillUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: The Raven Quill\n\n-- Color information for buttons and input boxes\nboxSize = 37\ninputFontsize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0705\n\ncustomizations = {\n [1] = {\n textField = {\n position = { 0.5, 0.25, -0.905 },\n width = 425\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.72,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.52,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.305,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = -0.105,\n count = 2,\n },\n textField = {\n position = { 0.125, 0.25, 0 },\n width = 775\n }\n },\n [6] = {\n checkboxes = {\n posZ = 0.1,\n count = 2,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.4,\n count = 3,\n }\n },\n [8] = {\n checkboxes = {\n posZ = 0.695,\n count = 4,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/TheRavenQuillUpgradeSheet\")\nend)\n__bundle_register(\"playercards/customizable/TheRavenQuillUpgradeSheet\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: The Raven Quill\n\n-- Color information for buttons and input boxes\nboxSize = 37\ninputFontsize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0705\n\ncustomizations = {\n [1] = {\n textField = {\n position = { 0.5, 0.25, -0.905 },\n width = 425\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.72,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.52,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.305,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = -0.105,\n count = 2,\n },\n textField = {\n position = { 0.125, 0.25, 0 },\n width = 775\n }\n },\n [6] = {\n checkboxes = {\n posZ = 0.1,\n count = 2,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.4,\n count = 3,\n }\n },\n [8] = {\n checkboxes = {\n posZ = 0.695,\n count = 4,\n }\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "CardCustom", @@ -181506,7 +125225,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/WendyAdams\")\nend)\n__bundle_register(\"playercards/cards/WendyAdams\", function(require, _LOADED, __bundle_register, __bundle_modules)\nbuttonHeight = \"320\"\nbuttonWidth = \"1100\"\nbuttonPosition = \"70 -70 -22\"\nbuttonFontSize = 200\nbuttonRotation = \"0 0 90\"\n\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -181995,12 +125714,12 @@ ], "Tooltip": true, "Transform": { - "posX": 40.7253036, - "posY": 1.29860592, - "posZ": 66.7765656, - "rotX": 1.697304e-7, - "rotY": 270.0102, - "rotZ": 2.00479718e-7, + "posX": 40, + "posY": 1.3, + "posZ": 66, + "rotX": 0, + "rotY": 270, + "rotZ": 0, "scaleX": 0.6, "scaleY": 1, "scaleZ": 0.6 @@ -182304,7 +126023,7 @@ }, "Description": "The Dilettante", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02003\",\n \"alternate_ids\": [\n \"98001\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98002\": 1,\n \"02010\": 1\n },\n {\n \"98003\": 1,\n \"02011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Rogue or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02003\",\n \"alternate_ids\": [\n \"98001\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98002\": 1,\n \"02010\": 1\n },\n {\n \"98003\": 1,\n \"02011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Rogue or Neutral\"\n }\n ]\n}", "GUID": "9058d3", "Grid": true, "GridProjection": false, @@ -182347,7 +126066,7 @@ }, "Description": "The Dilettante", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02003\",\n \"alternate_ids\": [\n \"98001\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98002\": 1,\n \"02010\": 1\n },\n {\n \"98003\": 1,\n \"02011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Rogue or Neutral\"\n }\n ]\n}\n", + "GMNotes": "{\n \"id\": \"02003\",\n \"alternate_ids\": [\n \"98001\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\r\n \"extraToken\": \"None\"\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98002\": 1,\n \"02010\": 1\n },\n {\n \"98003\": 1,\n \"02011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Rogue or Neutral\"\n }\n ]\n}\n", "GUID": "b954f6", "Grid": true, "GridProjection": false, @@ -182430,7 +126149,7 @@ }, "Description": "The Psychologist", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"05001\",\n \"alternate_ids\": [\n \"98010\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Medic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Circle Undone\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05007\": 1,\n \"98011\": 1\n },\n {\n \"05008\": 1,\n \"98012\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"weapon\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"special\": [\n \"heals_horror\"\n ],\n \"tag\": [\n \"hh\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"limit\": 15,\n \"error\": \"You cannot have more than 15 level 0-1 Seeker and/or Mystic cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"05001\",\n \"alternate_ids\": [\n \"98010\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Medic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"The Circle Undone\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"05007\": 1,\n \"98011\": 1\n },\n {\n \"05008\": 1,\n \"98012\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"weapon\"\n ],\n \"level\": {\n \"min\": 1,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"special\": [\n \"heals_horror\"\n ],\n \"tag\": [\n \"hh\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"limit\": 15,\n \"error\": \"You cannot have more than 15 level 0-1 Seeker and/or Mystic cards\"\n }\n ]\n}", "GUID": "b03b12", "Grid": true, "GridProjection": false, @@ -182473,7 +126192,7 @@ }, "Description": "The Psychologist", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"05001\",\r\n \"alternate_ids\": [\r\n \"98010\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Guardian\",\r\n \"traits\": \"Medic.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 4,\r\n \"combatIcons\": 2,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"The Circle Undone\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"05007\": 1,\r\n \"98011\": 1\r\n },\r\n {\r\n \"05008\": 1,\r\n \"98012\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"not\": true,\r\n \"trait\": [\r\n \"weapon\"\r\n ],\r\n \"level\": {\r\n \"min\": 1,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"guardian\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 3\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"special\": [\r\n \"heals_horror\"\r\n ],\r\n \"tag\": [\r\n \"hh\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"seeker\",\r\n \"mystic\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 1\r\n },\r\n \"limit\": 15,\r\n \"error\": \"You cannot have more than 15 level 0-1 Seeker and/or Mystic cards\"\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"05001\",\r\n \"alternate_ids\": [\r\n \"98010\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Guardian\",\r\n \"traits\": \"Medic.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 4,\r\n \"combatIcons\": 2,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"The Circle Undone\",\r\n \"extraToken\": \"None\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"05007\": 1,\r\n \"98011\": 1\r\n },\r\n {\r\n \"05008\": 1,\r\n \"98012\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"not\": true,\r\n \"trait\": [\r\n \"weapon\"\r\n ],\r\n \"level\": {\r\n \"min\": 1,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"guardian\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 3\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"special\": [\r\n \"heals_horror\"\r\n ],\r\n \"tag\": [\r\n \"hh\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"seeker\",\r\n \"mystic\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 1\r\n },\r\n \"limit\": 15,\r\n \"error\": \"You cannot have more than 15 level 0-1 Seeker and/or Mystic cards\"\r\n }\r\n ]\r\n}\r\n", "GUID": "9900a3", "Grid": true, "GridProjection": false, @@ -182556,7 +126275,7 @@ }, "Description": "The Magician", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"07004\",\n \"alternate_ids\": [\n \"98016\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer. Veteran.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98017\": 1,\n \"07012\": 1\n },\n {\n \"98018\": 1,\n \"07013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"07004\",\n \"alternate_ids\": [\n \"98016\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer. Veteran.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98017\": 1,\n \"07012\": 1\n },\n {\n \"98018\": 1,\n \"07013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "e015f8", "Grid": true, "GridProjection": false, @@ -182599,7 +126318,7 @@ }, "Description": "The Magician", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"07004\",\r\n \"alternate_ids\": [\r\n \"98016\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Mystic\",\r\n \"traits\": \"Sorcerer. Veteran.\",\r\n \"willpowerIcons\": 5,\r\n \"intellectIcons\": 2,\r\n \"combatIcons\": 3,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"The Innsmouth Conspiracy\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"98017\": 1,\r\n \"07012\": 1\r\n },\r\n {\r\n \"98018\": 1,\r\n \"07013\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"mystic\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"rogue\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"07004\",\r\n \"alternate_ids\": [\r\n \"98016\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Mystic\",\r\n \"traits\": \"Sorcerer. Veteran.\",\r\n \"willpowerIcons\": 5,\r\n \"intellectIcons\": 2,\r\n \"combatIcons\": 3,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"The Innsmouth Conspiracy\",\r\n \"extraToken\": \"Lightning\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"98017\": 1,\r\n \"07012\": 1\r\n },\r\n {\r\n \"98018\": 1,\r\n \"07013\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"mystic\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"rogue\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "3925ce", "Grid": true, "GridProjection": false, @@ -182682,7 +126401,7 @@ }, "Description": "The Sailor", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"07005\",\n \"alternate_ids\": [\n \"98013\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 4,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98014\": 1,\n \"07014\": 1,\n \"07015\": 1\n },\n {\n \"98015\": 1,\n \"07016\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"innate\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"07005\",\n \"alternate_ids\": [\n \"98013\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 2,\n \"combatIcons\": 4,\n \"agilityIcons\": 4,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"98014\": 1,\n \"07014\": 1,\n \"07015\": 1\n },\n {\n \"98015\": 1,\n \"07016\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"innate\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "3f92cf", "Grid": true, "GridProjection": false, @@ -182725,7 +126444,7 @@ }, "Description": "The Sailor", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"07005\",\r\n \"alternate_ids\": [\r\n \"98013\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Survivor\",\r\n \"traits\": \"Drifter.\",\r\n \"willpowerIcons\": 2,\r\n \"intellectIcons\": 2,\r\n \"combatIcons\": 4,\r\n \"agilityIcons\": 4,\r\n \"cycle\": \"The Innsmouth Conspiracy\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"98014\": 1,\r\n \"07014\": 1,\r\n \"07015\": 1\r\n },\r\n {\r\n \"98015\": 1,\r\n \"07016\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"survivor\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"trait\": [\r\n \"innate\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"07005\",\r\n \"alternate_ids\": [\r\n \"98013\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Survivor\",\r\n \"traits\": \"Drifter.\",\r\n \"willpowerIcons\": 2,\r\n \"intellectIcons\": 2,\r\n \"combatIcons\": 4,\r\n \"agilityIcons\": 4,\r\n \"cycle\": \"The Innsmouth Conspiracy\",\r\n \"extraToken\": \"Reaction\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"98014\": 1,\r\n \"07014\": 1,\r\n \"07015\": 1\r\n },\r\n {\r\n \"98015\": 1,\r\n \"07016\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"survivor\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"trait\": [\r\n \"innate\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "cd3308", "Grid": true, "GridProjection": false, @@ -182808,7 +126527,7 @@ }, "Description": "The Fed", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01001\",\n \"alternate_ids\": [\n \"98004\",\n \"01501\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01001\",\n \"alternate_ids\": [\n \"98004\",\n \"01501\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Guardian\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 4,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90030\": 1,\n \"98005\": 1,\n \"01006\": 1\n },\n {\n \"90031\": 1,\n \"98006\": 1,\n \"01007\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"guardian\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "9e9e98", "Grid": true, "GridProjection": false, @@ -182851,7 +126570,7 @@ }, "Description": "The Fed", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"01001\",\r\n \"alternate_ids\": [\r\n \"98004\",\r\n \"01501\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Guardian\",\r\n \"traits\": \"Agency. Detective.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 4,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"Core\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90030\": 1,\r\n \"98005\": 1,\r\n \"01006\": 1\r\n },\r\n {\r\n \"90031\": 1,\r\n \"98006\": 1,\r\n \"01007\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"guardian\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"seeker\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"01001\",\r\n \"alternate_ids\": [\r\n \"98004\",\r\n \"01501\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Guardian\",\r\n \"traits\": \"Agency. Detective.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 4,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"Core\",\r\n \"extraToken\": \"Reaction\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90030\": 1,\r\n \"98005\": 1,\r\n \"01006\": 1\r\n },\r\n {\r\n \"90031\": 1,\r\n \"98006\": 1,\r\n \"01007\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"guardian\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"seeker\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "a684e0", "Grid": true, "GridProjection": false, @@ -182913,7 +126632,7 @@ }, "Description": "The Fed", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"01001\",\r\n \"alternate_ids\": [\r\n \"98004\",\r\n \"01501\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Guardian\",\r\n \"traits\": \"Agency. Detective.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 4,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"Core\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90030\": 1,\r\n \"98005\": 1,\r\n \"01006\": 1\r\n },\r\n {\r\n \"90031\": 1,\r\n \"98006\": 1,\r\n \"01007\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"guardian\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"seeker\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"01001\",\r\n \"alternate_ids\": [\r\n \"98004\",\r\n \"01501\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Guardian\",\r\n \"traits\": \"Agency. Detective.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 4,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"Core\",\r\n \"extraToken\": \"Reaction\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90030\": 1,\r\n \"98005\": 1,\r\n \"01006\": 1\r\n },\r\n {\r\n \"90031\": 1,\r\n \"98006\": 1,\r\n \"01007\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"guardian\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"seeker\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "e46857", "Grid": true, "GridProjection": false, @@ -182996,7 +126715,7 @@ }, "Description": "The Ex-Con", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01003\",\n \"alternate_ids\": [\n \"01503\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01003\",\n \"alternate_ids\": [\n \"01503\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Criminal.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90009\": 1,\n \"01010\": 1\n },\n {\n \"90010\": 1,\n \"01011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"guardian\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "9015b4", "Grid": true, "GridProjection": false, @@ -183039,7 +126758,7 @@ }, "Description": "The Ex-Con", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"01003\",\r\n \"alternate_ids\": [\r\n \"01503\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Rogue\",\r\n \"traits\": \"Criminal.\",\r\n \"willpowerIcons\": 2,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 3,\r\n \"agilityIcons\": 4,\r\n \"cycle\": \"Core\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90009\": 1,\r\n \"01010\": 1\r\n },\r\n {\r\n \"90010\": 1,\r\n \"01011\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"rogue\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"guardian\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"01003\",\r\n \"alternate_ids\": [\r\n \"01503\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Rogue\",\r\n \"traits\": \"Criminal.\",\r\n \"willpowerIcons\": 2,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 3,\r\n \"agilityIcons\": 4,\r\n \"cycle\": \"Core\",\r\n \"extraToken\": \"Lightning\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90009\": 1,\r\n \"01010\": 1\r\n },\r\n {\r\n \"90010\": 1,\r\n \"01011\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"rogue\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"guardian\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "a41f81", "Grid": true, "GridProjection": false, @@ -183122,7 +126841,7 @@ }, "Description": "The Waitress", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01004\",\n \"alternate_ids\": [\n \"01504\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01004\",\n \"alternate_ids\": [\n \"01504\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Sorcerer.\",\n \"willpowerIcons\": 5,\n \"intellectIcons\": 2,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"Core\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"01012\": 1,\n \"90018\": 1\n },\n {\n \"01013\": 1,\n \"90019\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "25e2db", "Grid": true, "GridProjection": false, @@ -183165,7 +126884,7 @@ }, "Description": "The Waitress", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"01004\",\r\n \"alternate_ids\": [\r\n \"01504\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Mystic\",\r\n \"traits\": \"Sorcerer.\",\r\n \"willpowerIcons\": 5,\r\n \"intellectIcons\": 2,\r\n \"combatIcons\": 2,\r\n \"agilityIcons\": 3,\r\n \"cycle\": \"Core\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"01012\": 1,\r\n \"90018\": 1\r\n },\r\n {\r\n \"01013\": 1,\r\n \"90019\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"mystic\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"survivor\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"01004\",\r\n \"alternate_ids\": [\r\n \"01504\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Mystic\",\r\n \"traits\": \"Sorcerer.\",\r\n \"willpowerIcons\": 5,\r\n \"intellectIcons\": 2,\r\n \"combatIcons\": 2,\r\n \"agilityIcons\": 3,\r\n \"cycle\": \"Core\",\r\n \"extraToken\": \"Reaction\", \r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"01012\": 1,\r\n \"90018\": 1\r\n },\r\n {\r\n \"01013\": 1,\r\n \"90019\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"mystic\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"survivor\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "6797bb", "Grid": true, "GridProjection": false, @@ -183248,7 +126967,7 @@ }, "Description": "The Urchin", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01005\",\n \"alternate_ids\": [\n \"01505\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01005\",\n \"alternate_ids\": [\n \"01505\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Drifter.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 1,\n \"agilityIcons\": 4,\n \"cycle\": \"Core\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90039\": 1,\n \"01014\": 1\n },\n {\n \"90040\": 1,\n \"01015\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "fc1d17", "Grid": true, "GridProjection": false, @@ -183257,7 +126976,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/WendyAdams\")\nend)\n__bundle_register(\"playercards/cards/WendyAdams\", function(require, _LOADED, __bundle_register, __bundle_modules)\nbuttonHeight = \"320\"\nbuttonWidth = \"1100\"\nbuttonPosition = \"70 -70 -22\"\nbuttonFontSize = 200\nbuttonRotation = \"0 0 90\"\n\nrequire(\"playercards/CardsWithHelper\")\nrequire(\"playercards/CardsThatRedrawTokens\")\nend)\n__bundle_register(\"playercards/CardsThatRedrawTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that return and redraw tokens\nThis file is used to add an XML button to a card, turned on via context menu.\nValid options modify the appearance of the XML button, as well as the\nbehavior of the return and redraw function. Set options before requiring this file.\n\nParameters for the return and redraw functions. Typically set VALID_TOKENS or INVALID_TOKENS, not both.\nIf there are no restrictions on which tokens can be redrawn (e.g. Wendy Adams), keep both empty.\n* VALID_TOKENS --@type table\n - keyed table which lists all tokens that can be redrawn by the card\n - example usage: \"False Covenant\"\n \u003e VALID_TOKENS = {\n \u003e [\"Curse\"] = true\n \u003e }\n\n* INVALID_TOKENS --@type table\n - keyed table which lists all tokens that cannot be redrawn by the card\n - example usage: \"Custom Ammunition\"\n \u003e INVALID_TOKENS = {\n \u003e [\"Auto-fail\"] = true\n \u003e }\n\n* DRAW_SPECIFIC_TOKEN --@type string (name of token or nil)\n - if set, will attempt to draw that specific token\n\n* RETURN_TO_POOL --@type string\n - allows for the name of the card to be passed onto Global for any special handling\n\nThe following parameters modify the appearence of the XML button and are not listed as part of a table.\n - buttonHeight (default is 450)\n - buttonWidth (default is 1400)\n - buttonPosition (default is \"0 -55 -22\")\n - buttonFontSize (default is 250)\n - buttonRotation (change if button is placed on an investigator cards)\n - buttonLabel (default is \"Redraw Token\")\n - buttonIcon (to add an icon to the right)\n - buttonColor (default is \"#77674DE6\")\n\n----------------------------------------------------------\nEXAMPLE: Claypool's Furs\nThis card can only redraw the Frost token, and is replaced with a random token from the bag.\nAs a nice reminder the XML button takes on the Frost color and icon with the text \"Cancel\".\n \u003e buttonValue = \"Cancel\"\n \u003e buttonIcon = \"token-frost\"\n \u003e buttonColor = \"#404450E6\"\n \u003e buttonFontSize = 300\n\n \u003e VALID_TOKENS = {\n \u003e [\"Frost\"] = true\n \u003e }\n \u003e\n \u003e require...\n----------------------------------------------------------]]\n\n-- intentionally global\nhasXML = true\nisHelperEnabled = false\n\nfunction updateSave()\n self.script_state = JSON.encode({ isHelperEnabled = isHelperEnabled })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n isHelperEnabled = loadedData.isHelperEnabled\n end\n createHelperXML()\n syncDisplayWithOptionPanel()\nend\n\nfunction createHelperXML()\n local xmlTable = { {\n tag = \"Button\",\n attributes = {\n active = \"false\",\n id = \"Helper\",\n height = buttonHeight or 450,\n width = buttonWidth or 1400,\n rotation = buttonRotation or \"0 0 180\",\n scale = \"0.1 0.1 1\",\n position = buttonPosition or \"0 -55 -22\",\n padding = \"50 50 50 50\",\n font = \"font_teutonic-arkham\",\n fontSize = buttonFontSize or 250,\n onClick = \"triggerXMLTokenLabelCreation\",\n color = buttonColor or \"#77674DE6\",\n textColor = \"White\"\n },\n value = buttonLabel or \"Redraw Token\"\n } }\n if buttonIcon then\n xmlTable[1].attributes.iconWidth = \"400\"\n xmlTable[1].attributes.iconAlignment = \"Right\"\n xmlTable[1].attributes.icon = buttonIcon\n end\n self.UI.setXmlTable(xmlTable)\nend\n\nfunction triggerXMLTokenLabelCreation()\n Global.call(\"activeRedrawEffect\", {\n VALID_TOKENS = VALID_TOKENS,\n INVALID_TOKENS = INVALID_TOKENS,\n RETURN_TO_POOL = RETURN_TO_POOL\n })\nend\nend)\n__bundle_register(\"playercards/CardsWithHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that have helpers\nThis file is used to share code between cards with helpers.\nIt syncs the visibility of the helper with the option panel and\nmakes sure the card has the respective tag.\nAdditionally, it will call 'initiliaze()' and 'shutOff()'\nin the parent file if they are present.\n\nInstructions:\n1) Define the global variables before requiring this file:\nhasXML = true (whether the card has an XML display)\nisHelperEnabled = false (default state of the helper, should be 'false')\n\n2) In 'onLoad()'', call 'syncDisplayWithOptionPanel()'\n----------------------------------------------------------]]\n\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\n\n-- if the respective option is enabled in onLoad(), enable the helper\nfunction syncDisplayWithOptionPanel()\n self.addTag(\"CardWithHelper\")\n local options = optionPanelApi.getOptions()\n if options.enableCardHelpers then\n setHelperState(true)\n else\n updateDisplay()\n end\nend\n\n-- forces a new state\nfunction setHelperState(newState)\n isHelperEnabled = newState\n updateSave()\n updateDisplay()\nend\n\n-- toggles the current state\nfunction toggleHelper()\n isHelperEnabled = not isHelperEnabled\n updateSave()\n updateDisplay()\nend\n\n-- updates the visibility and calls events (after a small delay to allow XML being set)\nfunction updateDisplay()\n Wait.frames(actualDisplayUpdate, 5)\nend\n\nfunction actualDisplayUpdate()\n if isHelperEnabled then\n self.clearContextMenu()\n self.addContextMenuItem(\"Disable Helper\", toggleHelper)\n if hasXML then self.UI.show(\"Helper\") end\n if initialize then initialize() end\n else\n self.clearContextMenu()\n self.addContextMenuItem(\"Enable Helper\", toggleHelper)\n if hasXML then self.UI.hide(\"Helper\") end\n if shutOff then shutOff() end\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -183291,7 +127010,7 @@ }, "Description": "The Urchin", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"01005\",\r\n \"alternate_ids\": [\r\n \"01505\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Survivor\",\r\n \"traits\": \"Drifter.\",\r\n \"willpowerIcons\": 4,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 1,\r\n \"agilityIcons\": 4,\r\n \"cycle\": \"Core\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90039\": 1,\r\n \"01014\": 1\r\n },\r\n {\r\n \"90040\": 1,\r\n \"01015\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"survivor\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"rogue\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"01005\",\r\n \"alternate_ids\": [\r\n \"01505\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Survivor\",\r\n \"traits\": \"Drifter.\",\r\n \"willpowerIcons\": 4,\r\n \"intellectIcons\": 3,\r\n \"combatIcons\": 1,\r\n \"agilityIcons\": 4,\r\n \"cycle\": \"Core\",\r\n \"extraToken\": \"None\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90039\": 1,\r\n \"01014\": 1\r\n },\r\n {\r\n \"90040\": 1,\r\n \"01015\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"survivor\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"rogue\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "11bcb3", "Grid": true, "GridProjection": false, @@ -183374,7 +127093,7 @@ }, "Description": "The Librarian", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"01002\",\n \"alternate_ids\": [\n \"01502\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"01002\",\n \"alternate_ids\": [\n \"01502\"\n ],\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Miskatonic.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 2,\n \"agilityIcons\": 2,\n \"cycle\": \"Core\",\n \"extraToken\": \"Tome\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90002\": 1,\n \"01008\": 1\n },\n {\n \"90003\": 1,\n \"01009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "6938eb", "Grid": true, "GridProjection": false, @@ -183417,7 +127136,7 @@ }, "Description": "The Librarian", "DragSelectable": true, - "GMNotes": "{\r\n \"id\": \"01002\",\r\n \"alternate_ids\": [\r\n \"01502\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Seeker\",\r\n \"traits\": \"Miskatonic.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 5,\r\n \"combatIcons\": 2,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"Core\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90002\": 1,\r\n \"01008\": 1\r\n },\r\n {\r\n \"90003\": 1,\r\n \"01009\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"seeker\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"mystic\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", + "GMNotes": "{\r\n \"id\": \"01002\",\r\n \"alternate_ids\": [\r\n \"01502\"\r\n ],\r\n \"type\": \"Investigator\",\r\n \"class\": \"Seeker\",\r\n \"traits\": \"Miskatonic.\",\r\n \"willpowerIcons\": 3,\r\n \"intellectIcons\": 5,\r\n \"combatIcons\": 2,\r\n \"agilityIcons\": 2,\r\n \"cycle\": \"Core\",\r\n \"extraToken\": \"Tome\",\r\n \"deck_requirements\": {\r\n \"size\": 30,\r\n \"randomBasicWeaknessCount\": 1,\r\n \"signatures\": [\r\n {\r\n \"90002\": 1,\r\n \"01008\": 1\r\n },\r\n {\r\n \"90003\": 1,\r\n \"01009\": 1\r\n }\r\n ]\r\n },\r\n \"deck_options\": [\r\n {\r\n \"faction\": [\r\n \"seeker\",\r\n \"neutral\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 5\r\n }\r\n },\r\n {\r\n \"faction\": [\r\n \"mystic\"\r\n ],\r\n \"level\": {\r\n \"min\": 0,\r\n \"max\": 2\r\n }\r\n }\r\n ]\r\n}\r\n", "GUID": "ac7047", "Grid": true, "GridProjection": false, @@ -183704,7 +127423,7 @@ "Snap": true, "Sticky": true, "Tags": [ - "ScenarioCard" + "PlayerCard" ], "Tooltip": true, "Transform": { @@ -183986,7 +127705,7 @@ "551": { "BackIsHidden": true, "BackURL": "http://cloud-3.steamusercontent.com/ugc/2021607899142907490/4AAE686A793E66311FF78890309D20670A329D16/", - "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2021607899142907034/013ED775CA50C6FC71731E4FBAEBF1ECA8C68F1E/", + "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2450601300753083072/7500D69C546D9FD62750C45062986AE34060A8B1/", "NumHeight": 2, "NumWidth": 2, "Type": 0, @@ -183995,7 +127714,7 @@ }, "Description": "The Researcher", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"06002-t\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Assistant. Scholar.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 1,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dream-Eaters\",\n \"deck_requirements\": {\n \"size\": 50,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06008\": 3\n },\n {\n \"06009\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Rogue\",\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"06002-t\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Assistant. Scholar.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 5,\n \"combatIcons\": 1,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dream-Eaters\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 50,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"06008\": 3\n },\n {\n \"06009\": 1\n }\n ],\n \"choices\": 1\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"choiceName\": \"Mystic\",\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Rogue\",\n \"faction\": [\n \"rogue\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n },\n {\n \"choiceName\": \"Survivor\",\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 1\n },\n \"type\": [\n \"event\",\n \"skill\"\n ],\n \"limit\": 10\n }\n ]\n}", "GUID": "4f3637", "Grid": true, "GridProjection": false, @@ -184048,7 +127767,7 @@ "551": { "BackIsHidden": true, "BackURL": "http://cloud-3.steamusercontent.com/ugc/2021607899142907490/4AAE686A793E66311FF78890309D20670A329D16/", - "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2021607899142907034/013ED775CA50C6FC71731E4FBAEBF1ECA8C68F1E/", + "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2450601300753083072/7500D69C546D9FD62750C45062986AE34060A8B1/", "NumHeight": 2, "NumWidth": 2, "Type": 0, @@ -184057,7 +127776,7 @@ }, "Description": "The Spy", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"07003-t\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07010\": 1\n },\n {\n \"07011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"07003-t\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Agency. Detective.\",\n \"willpowerIcons\": 2,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Innsmouth Conspiracy\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"07010\": 1\n },\n {\n \"07011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"seeker\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n }\n }\n ]\n}", "GUID": "2ce76d", "Grid": true, "GridProjection": false, @@ -184086,9 +127805,9 @@ "rotX": 0, "rotY": 180, "rotZ": 0, - "scaleX": 1, + "scaleX": 1.15, "scaleY": 1, - "scaleZ": 1 + "scaleZ": 1.15 }, "Value": 0, "XmlUI": "" @@ -184110,7 +127829,7 @@ "551": { "BackIsHidden": true, "BackURL": "http://cloud-3.steamusercontent.com/ugc/2021607899142907490/4AAE686A793E66311FF78890309D20670A329D16/", - "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2021607899142907034/013ED775CA50C6FC71731E4FBAEBF1ECA8C68F1E/", + "FaceURL": "http://cloud-3.steamusercontent.com/ugc/2450601300753083072/7500D69C546D9FD62750C45062986AE34060A8B1/", "NumHeight": 2, "NumWidth": 2, "Type": 0, @@ -184119,7 +127838,7 @@ }, "Description": "The Actress", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"03006-t\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03018\": 2\n },\n {\n \"03019\": 2\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"guardian\",\n \"seeker\",\n \"rogue\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n },\n \"error\": \"You must have at least 7 cards from 3 different factions\"\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"03006-t\",\n \"type\": \"Investigator\",\n \"class\": \"Neutral\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 3,\n \"cycle\": \"The Path to Carcosa\",\n \"extraToken\": \"FreeTrigger\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"03018\": 2\n },\n {\n \"03019\": 2\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"survivor\",\n \"guardian\",\n \"seeker\",\n \"rogue\",\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n },\n \"error\": \"You must have at least 7 cards from 3 different factions\"\n },\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "52956d", "Grid": true, "GridProjection": false, @@ -184181,7 +127900,7 @@ }, "Description": "The Reporter", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02002-t\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02008\": 1\n },\n {\n \"02009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"fortune\"\n ]\n },\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Seeker or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02002-t\",\n \"type\": \"Investigator\",\n \"class\": \"Seeker\",\n \"traits\": \"Reporter.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 3,\n \"cycle\": \"The Dunwich Legacy\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"02008\": 1\n },\n {\n \"02009\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"not\": true,\n \"trait\": [\n \"fortune\"\n ]\n },\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Seeker or Neutral\"\n }\n ]\n}", "GUID": "0a5491", "Grid": true, "GridProjection": false, @@ -184252,7 +127971,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/RunicAxeUpgradeSheetTaboo\")\nend)\n__bundle_register(\"playercards/customizable/RunicAxeUpgradeSheetTaboo\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Runic Axe (Taboo)\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0705\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.92,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.715,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.415,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.018,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.265,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.66,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.86,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 1.065,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/RunicAxeUpgradeSheetTaboo\")\nend)\n__bundle_register(\"playercards/customizable/RunicAxeUpgradeSheetTaboo\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Runic Axe (Taboo)\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.935\nxOffset = 0.0705\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.92,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.715,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.415,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.018,\n count = 2,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.265,\n count = 1,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.66,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.86,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 1.065,\n count = 4,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "Card", @@ -184313,7 +128032,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/PowerWordUpgradeSheetTaboo\")\nend)\n__bundle_register(\"playercards/customizable/PowerWordUpgradeSheetTaboo\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Power Word (Taboo)\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.933\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.6,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.42,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.12,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.18,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.38,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.675,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.875,\n count = 3,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playmatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/customizable/PowerWordUpgradeSheetTaboo\")\nend)\n__bundle_register(\"playercards/customizable/PowerWordUpgradeSheetTaboo\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Customizable Cards: Power Word (Taboo)\n\n-- Color information for buttons\nboxSize = 38\n\n-- static values\nxInitial = -0.933\nxOffset = 0.069\n\ncustomizations = {\n [1] = {\n checkboxes = {\n posZ = -0.905,\n count = 1,\n }\n },\n [2] = {\n checkboxes = {\n posZ = -0.6,\n count = 1,\n }\n },\n [3] = {\n checkboxes = {\n posZ = -0.42,\n count = 1,\n }\n },\n [4] = {\n checkboxes = {\n posZ = -0.12,\n count = 1,\n }\n },\n [5] = {\n checkboxes = {\n posZ = 0.18,\n count = 2,\n },\n },\n [6] = {\n checkboxes = {\n posZ = 0.38,\n count = 3,\n }\n },\n [7] = {\n checkboxes = {\n posZ = 0.675,\n count = 3,\n },\n },\n [8] = {\n checkboxes = {\n posZ = 0.875,\n count = 3,\n },\n },\n}\n\nrequire(\"playercards/customizable/UpgradeSheetLibrary\")\nend)\n__bundle_register(\"playercards/customizable/UpgradeSheetLibrary\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Common code for handling customizable card upgrade sheets\n-- Define UI elements in the base card file, then include this\n-- UI element definition is an array of tables, each with this structure. A row may include\n-- checkboxes (number defined by count), a text field, both, or neither (if the row has custom\n-- handling, as Living Ink does)\n-- {\n-- checkboxes = {\n-- posZ = -0.71,\n-- count = 1,\n-- },\n-- textField = {\n-- position = { 0.005, 0.25, -0.58 },\n-- width = 875\n-- }\n-- }\n-- Fields should also be defined for xInitial (left edge of the checkboxes) and xOffset (amount to\n-- shift X from one box to the next) as well as boxSize (checkboxes) and inputFontSize.\n--\n-- selectedUpgrades holds the state of checkboxes and text input, each element being:\n-- selectedUpgrades[row] = { xp = #, text = \"\" }\n\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- Y position for UI elements. Visibility of checkboxes moves the checkbox inside the card object\n-- when not selected.\nlocal Y_VISIBLE = 0.25\nlocal Y_INVISIBLE = -0.5\n\n-- Used for Summoned Servitor and Living Ink\nlocal VECTOR_COLOR = {\n unselected = { 0.5, 0.5, 0.5, 0.75 },\n mystic = { 0.597, 0.195, 0.796 }\n}\n\n-- These match with ArkhamDB's way of storing the data in the dropdown menu\nlocal SUMMONED_SERVITOR_SLOT_INDICES = { arcane = \"1\", ally = \"0\", none = \"\" }\n\nlocal rowCheckboxFirstIndex = { }\nlocal rowInputIndex = { }\nlocal selectedUpgrades = { }\n\n-- save state when going into bags / decks\nfunction onDestroy() self.script_state = onSave() end\n\nfunction onSave()\n return JSON.encode({\n selections = selectedUpgrades\n })\nend\n\n-- Startup procedure\nfunction onLoad(savedData)\n if savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n if loadedData.selections ~= nil then\n selectedUpgrades = loadedData.selections\n end\n end\n\n selfId = getSelfId()\n\n maybeLoadLivingInkSkills()\n createUi()\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\n\n self.addContextMenuItem(\"Clear Selections\", function() resetSelections() end)\n self.addContextMenuItem(\"Scale: 1x\", function() self.setScale({ 1, 1, 1 }) end)\n self.addContextMenuItem(\"Scale: 2x\", function() self.setScale({ 2, 1, 2 }) end)\n self.addContextMenuItem(\"Scale: 3x\", function() self.setScale({ 3, 1, 3 }) end)\nend\n\n-- Grabs the ID from the metadata for special functions (Living Ink, Summoned Servitor)\nfunction getSelfId()\n local metadata = JSON.decode(self.getGMNotes())\n return metadata.id\nend\n\nfunction isUpgradeActive(row)\n return customizations[row] ~= nil\n and customizations[row].checkboxes ~= nil\n and customizations[row].checkboxes.count ~= nil\n and customizations[row].checkboxes.count \u003e 0\n and selectedUpgrades[row] ~= nil\n and selectedUpgrades[row].xp ~= nil\n and selectedUpgrades[row].xp \u003e= customizations[row].checkboxes.count\nend\n\nfunction resetSelections()\n selectedUpgrades = { }\n updateDisplay()\nend\n\nfunction createUi()\n if customizations == nil then\n return\n end\n for i = 1, #customizations do\n if customizations[i].checkboxes ~= nil then\n createRowCheckboxes(i)\n end\n if customizations[i].textField ~= nil then\n createRowTextField(i)\n end\n end\n maybeMakeLivingInkSkillSelectionButtons()\n maybeMakeServitorSlotSelectionButtons()\n updateDisplay()\nend\n\nfunction createRowCheckboxes(rowIndex)\n local checkboxes = customizations[rowIndex].checkboxes\n rowCheckboxFirstIndex[rowIndex] = 0\n local previousButtons = self.getButtons()\n if previousButtons ~= nil then\n rowCheckboxFirstIndex[rowIndex] = #previousButtons\n end\n for col = 1, checkboxes.count do\n local funcName = \"checkboxRow\" .. rowIndex .. \"Col\" .. col\n local func = function() clickCheckbox(rowIndex, col) end\n self.setVar(funcName, func)\n local checkboxPos = getCheckboxPosition(rowIndex, col)\n\n self.createButton({\n click_function = funcName,\n function_owner = self,\n position = checkboxPos,\n height = boxSize * 10,\n width = boxSize * 10,\n font_size = 1000,\n scale = { 0.1, 0.1, 0.1 },\n color = { 0, 0, 0 },\n font_color = { 0, 0, 0 }\n })\n end\nend\n\nfunction getCheckboxPosition(row, col)\n return {\n x = xInitial + col * xOffset,\n y = Y_VISIBLE,\n z = customizations[row].checkboxes.posZ\n }\nend\n\nfunction createRowTextField(rowIndex)\n local textField = customizations[rowIndex].textField\n\n rowInputIndex[rowIndex] = 0\n local previousInputs = self.getInputs()\n if previousInputs ~= nil then\n rowInputIndex[rowIndex] = #previousInputs\n end\n local funcName = \"textbox\" .. rowIndex\n local func = function(_, _, val, sel) clickTextbox(rowIndex, val, sel) end\n self.setVar(funcName, func)\n\n self.createInput({\n input_function = funcName,\n function_owner = self,\n label = \"Click to type\",\n alignment = 2,\n position = textField.position,\n scale = { 0.1, 0.1, 0.1 },\n width = textField.width * 10,\n height = inputFontsize * 10 + 75,\n font_size = inputFontsize * 10.5,\n color = \"White\",\n value = \"\"\n })\nend\n\nfunction updateDisplay()\n for i = 1, #customizations do\n updateRowDisplay(i)\n end\n maybeUpdateLivingInkSkillDisplay()\n maybeUpdateServitorSlotDisplay()\nend\n\nfunction updateRowDisplay(rowIndex)\n if customizations[rowIndex].checkboxes ~= nil then\n updateCheckboxes(rowIndex)\n end\n if customizations[rowIndex].textField ~= nil then\n updateTextField(rowIndex)\n end\nend\n\nfunction updateCheckboxes(rowIndex)\n local checkboxCount = customizations[rowIndex].checkboxes.count\n local selected = 0\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].xp ~= nil then\n selected = selectedUpgrades[rowIndex].xp\n end\n local checkboxIndex = rowCheckboxFirstIndex[rowIndex]\n for col = 1, checkboxCount do\n local pos = getCheckboxPosition(rowIndex, col)\n if col \u003c= selected then\n pos.y = Y_VISIBLE\n else\n pos.y = Y_INVISIBLE\n end\n self.editButton({\n index = checkboxIndex,\n position = pos\n })\n checkboxIndex = checkboxIndex + 1\n end\nend\n\nfunction updateTextField(rowIndex)\n local inputIndex = rowInputIndex[rowIndex]\n if selectedUpgrades[rowIndex] ~= nil and selectedUpgrades[rowIndex].text ~= nil then\n self.editInput({\n index = inputIndex,\n value = \" \" .. selectedUpgrades[rowIndex].text\n })\n end\nend\n\nfunction clickCheckbox(row, col)\n if selectedUpgrades[row] == nil then\n selectedUpgrades[row] = { }\n selectedUpgrades[row].xp = 0\n end\n if selectedUpgrades[row].xp == col then\n selectedUpgrades[row].xp = col - 1\n else\n selectedUpgrades[row].xp = col\n end\n updateCheckboxes(row)\n playermatApi.syncAllCustomizableCards()\nend\n\n-- Updates saved value for given text box when it loses focus\nfunction clickTextbox(rowIndex, value, selected)\n if selected == false then\n if selectedUpgrades[rowIndex] == nil then\n selectedUpgrades[rowIndex] = { }\n end\n selectedUpgrades[rowIndex].text = value:gsub(\"^%s*(.-)%s*$\", \"%1\")\n -- Editing isn't actually done yet, and will block the update. Wait a frame so it's finished\n Wait.frames(function() updateRowDisplay(rowIndex) end, 1)\n end\nend\n\n---------------------------------------------------------\n-- Living Ink related functions\n---------------------------------------------------------\n\n-- Builds the list of boolean skill selections from the Row 1 text field\nfunction maybeLoadLivingInkSkills()\n if selfId ~= \"09079-c\" then return end\n selectedSkills = {\n willpower = false,\n intellect = false,\n combat = false,\n agility = false\n }\n if selectedUpgrades[1] ~= nil and selectedUpgrades[1].text ~= nil then\n for skill in string.gmatch(selectedUpgrades[1].text, \"([^,]+)\") do\n selectedSkills[skill] = true\n end\n end\nend\n\nfunction clickSkill(skillname)\n selectedSkills[skillname] = not selectedSkills[skillname]\n maybeUpdateLivingInkSkillDisplay()\n updateSelectedLivingInkSkillText()\nend\n\n-- Creates the invisible buttons overlaying the skill icons\nfunction maybeMakeLivingInkSkillSelectionButtons()\n if selfId ~= \"09079-c\" then return end\n\n local buttonData = {\n function_owner = self,\n position = { y = 0.2 },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n\n for skillname, _ in pairs(selectedSkills) do\n local funcName = \"clickSkill\" .. skillname\n self.setVar(funcName, function() clickSkill(skillname) end)\n\n buttonData.click_function = funcName\n buttonData.position.x = -1 * SKILL_ICON_POSITIONS[skillname].x\n buttonData.position.z = SKILL_ICON_POSITIONS[skillname].z\n self.createButton(buttonData)\n end\nend\n\n-- Builds a comma-delimited string of skills and places it in the Row 1 text field\nfunction updateSelectedLivingInkSkillText()\n local skillString = \"\"\n if selectedSkills.willpower then\n skillString = skillString .. \"willpower\" .. \",\"\n end\n if selectedSkills.intellect then\n skillString = skillString .. \"intellect\" .. \",\"\n end\n if selectedSkills.combat then\n skillString = skillString .. \"combat\" .. \",\"\n end\n if selectedSkills.agility then\n skillString = skillString .. \"agility\" .. \",\"\n end\n if selectedUpgrades[1] == nil then\n selectedUpgrades[1] = { }\n end\n selectedUpgrades[1].text = skillString\nend\n\n-- Refresh the vector circles indicating a skill is selected. Since we can only have one table of\n-- vectors set, have to refresh all 4 at once\nfunction maybeUpdateLivingInkSkillDisplay()\n if selfId ~= \"09079-c\" then return end\n local circles = {}\n for skill, isSelected in pairs(selectedSkills) do\n if isSelected then\n local circle = getCircleVector(SKILL_ICON_POSITIONS[skill])\n if circle ~= nil then\n table.insert(circles, circle)\n end\n end\n end\n self.setVectorLines(circles)\nend\n\nfunction getCircleVector(center)\n local diameter = Vector(0, 0, 0.1)\n local pointOfOrigin = Vector(center.x, Y_VISIBLE, center.z)\n local vec\n local vecList = {}\n local arcStep = 5\n for i = 0, 360, arcStep do\n diameter:rotateOver('y', arcStep)\n vec = pointOfOrigin + diameter\n vec.y = pointOfOrigin.y\n table.insert(vecList, vec)\n end\n\n return {\n points = vecList,\n color = VECTOR_COLOR.mystic,\n thickness = 0.02,\n }\nend\n\n---------------------------------------------------------\n-- Summoned Servitor related functions\n---------------------------------------------------------\n\n-- Creates the invisible buttons overlaying the slot words\nfunction maybeMakeServitorSlotSelectionButtons()\n if selfId ~= \"09080-c\" then return end\n\n local buttonData = {\n click_function = \"clickArcane\",\n function_owner = self,\n position = { x = -1 * SLOT_ICON_POSITIONS.arcane.x, y = 0.2, z = SLOT_ICON_POSITIONS.arcane.z },\n height = 130,\n width = 130,\n color = { 0, 0, 0, 0 },\n }\n self.createButton(buttonData)\n\n buttonData.click_function = \"clickAlly\"\n buttonData.position.x = -1 * SLOT_ICON_POSITIONS.ally.x\n self.createButton(buttonData)\nend\n\n-- toggles the clicked slot\nfunction clickArcane()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.arcane\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- toggles the clicked slot\nfunction clickAlly()\n if selectedUpgrades[6] == nil then\n selectedUpgrades[6] = { }\n end\n if selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.none\n else\n selectedUpgrades[6].text = SUMMONED_SERVITOR_SLOT_INDICES.ally\n end\n maybeUpdateServitorSlotDisplay()\nend\n\n-- Refresh the vector circles indicating a slot is selected.\nfunction maybeUpdateServitorSlotDisplay()\n if selfId ~= \"09080-c\" then return end\n\n local center = SLOT_ICON_POSITIONS[\"arcane\"]\n local arcaneVecList = {\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.12, Y_VISIBLE, center.z + 0.05),\n }\n\n center = SLOT_ICON_POSITIONS[\"ally\"]\n local allyVecList = {\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z + 0.05),\n Vector(center.x - 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z - 0.05),\n Vector(center.x + 0.07, Y_VISIBLE, center.z + 0.05),\n }\n\n local arcaneVecColor = VECTOR_COLOR.unselected\n local allyVecColor = VECTOR_COLOR.unselected\n\n if selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.arcane then\n arcaneVecColor = VECTOR_COLOR.mystic\n elseif selectedUpgrades[6] ~= nil and selectedUpgrades[6].text == SUMMONED_SERVITOR_SLOT_INDICES.ally then\n allyVecColor = VECTOR_COLOR.mystic\n end\n\n self.setVectorLines({\n {\n points = arcaneVecList,\n color = arcaneVecColor,\n thickness = 0.02,\n },\n {\n points = allyVecList,\n color = allyVecColor,\n thickness = 0.02,\n }\n })\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[0,0,0,0,0,0,0,0,0,0],[\"\",\"\",\"\",\"\",\"\"]]", "MeasureMovement": false, "Name": "Card", @@ -184978,7 +128697,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02266-t\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 0,\n \"level\": 3,\n \"traits\": \"Trick.\",\n \"cycle\": \"The Dunwich Legacy\"\n}", + "GMNotes": "{\n \"id\": \"02266-t\",\n \"type\": \"Event\",\n \"class\": \"Rogue\",\n \"cost\": 0,\n \"level\": 3,\n \"traits\": \"Trick.\",\n \"uses\": [\n {\n \"count\": 3,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Dunwich Legacy\"\n}", "GUID": "e92f21", "Grid": true, "GridProjection": false, @@ -185786,7 +129505,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/FluteoftheOuterGods4\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nSHOW_SINGLE_RELEASE = true\nKEEP_OPEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nValid options (set before requiring this file):\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_SINGLE_RELEASE --@type: boolean\n - enables an entry in the context menu\n - this entry allows releasing a single token\n - example usage: \"Holy Spear\" (to keep the other tokens and just release one)\n\nSHOW_MULTI_RELEASE --@type: number (amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once\n - example usage: \"Nephthys\" (to release 3 bless tokens at once)\n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_SINGLE_RELEASE = true\n \u003e SHOW_MULTI_SEAL = 2\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n -- conditional single or multi release options\n if SHOW_SINGLE_RELEASE then\n self.addContextMenuItem(\"Release token\", releaseOneToken)\n elseif SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n else\n self.addContextMenuItem(\"Release token(s)\", releaseAllTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = 1, SHOW_MULTI_SEAL do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release multiple tokens at once\nfunction releaseMultipleTokens(playerColor)\n if SHOW_MULTI_RELEASE \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RELEASE do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. SHOW_MULTI_RELEASE .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playmatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FluteoftheOuterGods4\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/FluteoftheOuterGods4\")\nend)\n__bundle_register(\"accessories/TokenArrangerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenArrangerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- local function to call the token arranger, if it is on the table\n ---@param functionName string Name of the function to cal\n ---@param argument? table Parameter to pass\n local function callIfExistent(functionName, argument)\n local tokenArranger = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenArranger\")\n if tokenArranger ~= nil then\n tokenArranger.call(functionName, argument)\n end\n end\n\n -- updates the token modifiers with the provided data\n ---@param fullData table Contains the chaos token metadata\n TokenArrangerApi.onTokenDataChanged = function(fullData)\n callIfExistent(\"onTokenDataChanged\", fullData)\n end\n\n -- deletes already laid out tokens\n TokenArrangerApi.deleteCopiedTokens = function()\n callIfExistent(\"deleteCopiedTokens\")\n end\n\n -- updates the laid out tokens\n TokenArrangerApi.layout = function()\n Wait.time(function() callIfExistent(\"layout\") end, 0.1)\n end\n\n return TokenArrangerApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"playercards/cards/FluteoftheOuterGods4\", function(require, _LOADED, __bundle_register, __bundle_modules)\nVALID_TOKENS = {\n [\"Curse\"] = true\n}\n\nMAX_SEALED = 10\nKEEP_OPEN = true\n\nrequire(\"playercards/CardsThatSealTokens\")\nend)\n__bundle_register(\"playercards/CardsThatSealTokens\", function(require, _LOADED, __bundle_register, __bundle_modules)\n--[[ Library for cards that seal tokens\nThis file is used to add sealing option to cards' context menu.\nNOTE: all cards are allowed to release a single token to enable Hallow and A Watchful Peace,\nand to release all sealed tokens to allow for cards that might leave play with sealed tokens on them.\nValid options (set before requiring this file):\n\nMAX_SEALED --@type: number (maximum number of tokens allowable by the card to be sealed)\n - required for all cards\n - if MAX_SEALED is more than 1, then an XML label is created for the topmost token indicating the number of sealed tokens\n - gives an error if user tries to seal additional tokens on the card\n - example usage: \"The Chthonian Stone\"\n \u003e MAX_SEALED = 1\n\nUPDATE_ON_HOVER --@type: boolean\n - automatically updates the context menu options when the card is hovered\n - the \"Read Bag\" function reads the content of the chaos bag to update the context menu\n - example usage: \"Unrelenting\" (to only display valid tokens)\n\nKEEP_OPEN --@type: boolean\n- meant for cards that seal single tokens multiple times (one by one)\n- makes the context menu stay open after selecting an option\n- example usage: \"Unrelenting\"\n\nSHOW_MULTI_RELEASE --@type: number (maximum amount of tokens to release at once)\n - enables an entry in the context menu\n - this entry allows releasing of multiple tokens at once, to the maximum number\n - does not fail if there are fewer than the maximum sealed\n - example usage: \"Nephthys\" (to release up to 3 bless tokens at once) \n\nSHOW_MULTI_RETURN --@type: number (amount of tokens to return to pool at once)\n - enables an entry in the context menu\n - this entry allows returning tokens to the token pool\n - fails if not enough tokens are sealed\n - example usage: \"Nephthys\" (to return 3 bless tokens at once)\n\nSHOW_MULTI_SEAL --@type: number (amount of tokens to seal at once)\n - enables an entry in the context menu\n - this entry allows sealing of multiple tokens at once\n - example usage: \"Holy Spear\" (to seal two bless tokens at once)\n\nVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens should be abled to be sealed\n - needs to be defined for each card -\u003e even if empty\n - example usage: \"The Chthonian Stone\"\n \u003e VALID_TOKENS = {\n \u003e [\"Skull\"] = true,\n \u003e [\"Cultist\"] = true,\n \u003e [\"Tablet\"] = true,\n \u003e [\"Elder Thing\"] = true,\n \u003e }\n\nINVALID_TOKENS --@type: table ([tokenName] = true)\n - this table defines which tokens are invalid for sealing\n - only needs to be defined if needed\n - usually combined with empty \"VALID_TOKENS\" table\n - example usage: \"Protective Incantation\" (not allowed to seal Auto-fail)\n\n----------------------------------------------------------\nExample 1: Crystalline Elder Sign\nThis card can only seal the \"+1\" or \"Elder Sign\" token,\nit does not need specific options for multi-sealing or releasing.\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"+1\"] = true,\n \u003e [\"Elder Sign\"] = true\n \u003e }\n \u003e MAX_SEALED = 1\n \u003e require...\n----------------------------------------------------------\nExample 2: Holy Spear\nThis card features the following abilities (just listing the relevant parts):\n- releasing a single bless token\n- sealing two bless tokens\nThus it should be implemented like this:\n \u003e VALID_TOKENS = {\n \u003e [\"Bless\"] = true\n \u003e }\n \u003e SHOW_MULTI_SEAL = 2\n \u003e MAX_SEALED = 10\n \u003e require...\n----------------------------------------------------------]]\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal tokenArrangerApi = require(\"accessories/TokenArrangerApi\")\n\nlocal sealedTokens = {}\nlocal ID_URL_MAP = {}\nlocal tokensInBag = {}\n\n-- XML background color for each token for label when stacked\nlocal tokenColor = {\n [\"Skull\"] = \"#4A0400E6\",\n [\"Cultist\"] = \"#173B0BE6\",\n [\"Tablet\"] = \"#1D2238E6\",\n [\"Elder Thing\"] = \"#4D2331E6\",\n [\"Auto-fail\"] = \"#9B0004E6\",\n [\"Bless\"] = \"#9D702CE6\",\n [\"Curse\"] = \"#633A84E6\",\n [\"Frost\"] = \"#404450E6\",\n [\"Elder Sign\"] = \"#50A8CEE6\",\n [\"\"] = \"#77674DE6\"\n}\n\nfunction onSave() return JSON.encode(sealedTokens) end\n\nfunction onLoad(savedData)\n sealedTokens = JSON.decode(savedData) or {}\n ID_URL_MAP = chaosBagApi.getIdUrlMap()\n generateContextMenu()\n self.addTag(\"CardThatSeals\")\nend\n\n-- builds the context menu\nfunction generateContextMenu()\n self.addContextMenuItem(\"Release one token\", releaseOneToken)\n\n -- conditional release options\n if MAX_SEALED \u003e 1 then\n self.addContextMenuItem(\"Release all tokens\", releaseAllTokens)\n end\n\n if SHOW_MULTI_RELEASE then\n self.addContextMenuItem(\"Release \" .. SHOW_MULTI_RELEASE .. \" token(s)\", releaseMultipleTokens)\n end\n\n if RESOLVE_TOKEN then\n local firstTokenType\n for tokenType, val in pairs(VALID_TOKENS) do\n firstTokenType = tokenType\n break\n end\n self.addContextMenuItem(\"Resolve \" .. firstTokenType, resolveSealed)\n end\n\n -- conditional release option\n if SHOW_MULTI_RETURN then\n self.addContextMenuItem(\"Return \" .. SHOW_MULTI_RETURN .. \" token(s)\", returnMultipleTokens)\n end\n\n -- main context menu options to seal tokens\n for _, map in pairs(ID_URL_MAP) do\n if (VALID_TOKENS[map.name] ~= nil) or (UPDATE_ON_HOVER and tokensInBag[map.name] and not INVALID_TOKENS[map.name]) then\n if not SHOW_MULTI_SEAL then\n self.addContextMenuItem(\"Seal \" .. map.name, function(playerColor)\n sealToken(map.name, playerColor)\n end, KEEP_OPEN)\n else\n self.addContextMenuItem(\"Seal \" .. SHOW_MULTI_SEAL .. \" \" .. map.name, function(playerColor)\n readBag()\n local allowed = true\n local notFound\n\n for name, _ in pairs(VALID_TOKENS) do\n if (tokensInBag[name] or 0) \u003c SHOW_MULTI_SEAL then\n allowed = false\n notFound = name\n end\n end\n\n if allowed then\n for i = SHOW_MULTI_SEAL, 1, -1 do\n sealToken(map.name, playerColor)\n end\n else\n printToColor(\"Not enough \" .. notFound .. \" tokens in the chaos bag.\", playerColor)\n end\n end)\n end\n end\n end\nend\n\n-- generates a list of chaos tokens that is in the chaos bag\nfunction readBag()\n local chaosbag = chaosBagApi.findChaosBag()\n tokensInBag = {}\n\n for _, token in ipairs(chaosbag.getObjects()) do\n tokensInBag[token.name] = (tokensInBag[token.name] or 0) + 1\n end\nend\n\nfunction resetSealedTokens()\n sealedTokens = {}\nend\n\n-- native event from TTS - used to update the context menu for cards like \"Unrelenting\"\nfunction onHover()\n if UPDATE_ON_HOVER then\n readBag()\n self.clearContextMenu()\n generateContextMenu()\n end\nend\n\n-- seals the named token on this card\nfunction sealToken(name, playerColor)\n if #sealedTokens \u003e= MAX_SEALED then\n printToColor(\"Cannot seal any more tokens on this card\", playerColor, \"Red\")\n return\n end\n if not chaosBagApi.canTouchChaosTokens() then return end\n local chaosbag = chaosBagApi.findChaosBag()\n for i, obj in ipairs(chaosbag.getObjects()) do\n if obj.name == name then\n chaosbag.takeObject({\n position = self.getPosition() + Vector(0, 0.5 + 0.1 * #sealedTokens, 0),\n rotation = self.getRotation(),\n index = i - 1,\n smooth = false,\n callback_function = function(token)\n local guid = token.getGUID()\n table.insert(sealedTokens, guid)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.sealedToken(name, guid)\n end\n -- destroy XML on just covered token\n if #sealedTokens \u003e 1 then\n local coveredToken = getObjectFromGUID(sealedTokens[#sealedTokens - 1])\n if coveredToken ~= nil then\n coveredToken.UI.setXml(\"\")\n else\n table.remove(sealedTokens, #sealedTokens - 1)\n end\n end\n updateStackSize()\n end\n })\n return\n end\n end\n printToColor(name .. \" token not found in chaos bag\", playerColor)\nend\n\n-- release the last sealed token\nfunction releaseOneToken(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token\", playerColor)\n putTokenAway(table.remove(sealedTokens))\n end\nend\n\n-- release up to multiple tokens at once with no minimum\nfunction releaseMultipleTokens(playerColor)\n if #sealedTokens == 0 then\n printToColor(\"Not enough tokens sealed.\", playerColor)\n return\n end\n\n local numRemoved = SHOW_MULTI_RELEASE\n if #sealedTokens \u003c SHOW_MULTI_RELEASE then\n numRemoved = #sealedTokens\n end\n\n for i = 1, numRemoved do\n putTokenAway(table.remove(sealedTokens))\n end\n printToColor(\"Releasing \" .. numRemoved .. \" tokens\", playerColor)\nend\n\n-- releases all sealed tokens\nfunction releaseAllTokens(playerColor)\n if not chaosBagApi.canTouchChaosTokens() then return end\n if #sealedTokens == 0 then\n printToColor(\"No sealed token(s) found\", playerColor)\n else\n printToColor(\"Releasing token(s)\", playerColor)\n for _, guid in ipairs(sealedTokens) do\n putTokenAway(guid)\n end\n sealedTokens = {}\n end\nend\n\n-- returns multiple tokens at once to the token pool\nfunction returnMultipleTokens(playerColor)\n if SHOW_MULTI_RETURN \u003c= #sealedTokens then\n for i = 1, SHOW_MULTI_RETURN do\n returnToken(table.remove(sealedTokens))\n end\n printToColor(\"Returning \" .. SHOW_MULTI_RETURN .. \" tokens\", playerColor)\n else\n printToColor(\"Not enough tokens sealed.\", playerColor)\n end\nend\n\n-- returns the token (referenced by GUID) to the chaos bag\nfunction putTokenAway(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n local chaosbag = chaosBagApi.findChaosBag()\n chaosbag.putObject(token)\n tokenArrangerApi.layout()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.releasedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- returns the token to the pool (== removes it)\nfunction returnToken(guid)\n local token = getObjectFromGUID(guid)\n if not token then return end\n\n local name = token.getName()\n token.destruct()\n if name == \"Bless\" or name == \"Curse\" then\n blessCurseManagerApi.returnedToken(name, guid)\n end\n updateStackSize()\nend\n\n-- resolves sealed token as if it came from the chaos bag\nfunction resolveSealed()\n if #sealedTokens == 0 then\n broadcastToAll(\"No tokens sealed.\", \"Red\")\n return\n end\n local closestMatColor = playermatApi.getMatColorByPosition(self.getPosition())\n local mat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local guidToBeResolved = table.remove(sealedTokens)\n local resolvedToken = getObjectFromGUID(guidToBeResolved)\n resolvedToken.UI.setXml(\"\")\n updateStackSize()\n chaosBagApi.drawChaosToken(mat, true, _, guidToBeResolved)\nend\n\nfunction updateStackSize()\n if MAX_SEALED == 1 then return end\n if #sealedTokens == 0 then return end\n -- get topmost sealed token\n local topToken = getObjectFromGUID(sealedTokens[#sealedTokens])\n local name = topToken.getName()\n\n topToken.UI.setXmlTable({\n {\n tag = \"Panel\",\n attributes = {\n height = 380,\n width = 380,\n rotation = \"0 0 180\",\n scale = \"0.2 0.2 1\",\n position = \"0 0 -12\",\n color = tokenColor[name] or \"#77674DE6\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n fontSize = \"380\",\n font = \"font_teutonic-arkham\",\n color = \"#ffffff\",\n outline = \"#000000\",\n outlineSize = \"8 -8\",\n text = \"x\" .. #sealedTokens\n }\n }\n }\n })\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -186093,7 +129812,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between the lvl 0 and lvl 3 versions of Scroll of Secrets\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- get class via metadata and create context menu accordingly\nfunction onLoad()\n local notes = JSON.decode(self.getGMNotes())\n if notes then\n createContextMenu(notes.id)\n else\n print(\"Missing metadata for Scroll of Secrets!\")\n end\nend\n\nfunction createContextMenu(id)\n if id == \"05116\" or id == \"05116-t\" then\n -- lvl 0: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n elseif id == \"05188\" or id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n self.addContextMenuItem(\"Draw bottom card(s)\", function(playerColor) contextFunc(playerColor, 3) end)\n elseif id == \"05189\" or id == \"05189-t\" then\n -- mystic lvl 3: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n end\nend\n\nfunction contextFunc(playerColor, amount)\n local options = { \"Encounter Deck\" }\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playmatApi.getMatColor(color)\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n table.insert(options, color)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _, function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n -- variable initialization\n local deck = nil\n local deckSize = 1\n local deckAreaObjects = {}\n\n -- get the respective deck\n if owner == \"Encounter Deck\" then\n deck = mythosAreaApi.getEncounterDeck()\n else\n local matColor = playmatApi.getMatColor(owner)\n deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n deck = deckAreaObjects.draw\n end\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n -- get class via metadata and proceed menu accordingly:\n -- lvl 0: draw 1 card from the bottom\n -- mystic lvl 3: draw 1 card from the bottom\n local buttonLabel = \"Draw bottom card\"\n local amount = 1\n local notes = JSON.decode(self.getGMNotes())\n if notes.id == \"05188\" or notes.id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n buttonLabel = buttonLabel .. \"(s)\"\n amount = 3\n end\n self.addContextMenuItem(buttonLabel, function(playerColor) contextFunc(playerColor, amount) end)\nend\n\nfunction contextFunc(playerColor, amount)\n Player[playerColor].clearSelectedObjects()\n deckData = {}\n local options = {}\n\n -- check for encounter deck\n local encounterDeck = mythosAreaApi.getEncounterDeck()\n if encounterDeck then\n table.insert(options, \"Encounter Deck\")\n deckData[\"Encounter Deck\"] = { draw = encounterDeck }\n end\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playermatApi.getMatColor(color)\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n local playerName = Player[color].steam_name\n local invName = playermatApi.getInvestigatorName(matColor)\n\n -- figure out a proper display name for the drop down menu\n local displayName = color .. \" (color)\"\n if playerName then\n displayName = playerName\n elseif invName ~= \"\" then\n displayName = invName .. \" (inv)\"\n end\n\n -- recreate table in this script to have access\n deckData[displayName] = {\n draw = deckAreaObjects.draw,\n topCard = deckAreaObjects.topCard\n }\n table.insert(options, displayName)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _,\n function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n local deckAreaObjects = deckData[owner]\n local deck = deckAreaObjects.draw\n local deckSize = 1\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -186771,7 +130490,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between the lvl 0 and lvl 3 versions of Scroll of Secrets\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- get class via metadata and create context menu accordingly\nfunction onLoad()\n local notes = JSON.decode(self.getGMNotes())\n if notes then\n createContextMenu(notes.id)\n else\n print(\"Missing metadata for Scroll of Secrets!\")\n end\nend\n\nfunction createContextMenu(id)\n if id == \"05116\" or id == \"05116-t\" then\n -- lvl 0: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n elseif id == \"05188\" or id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n self.addContextMenuItem(\"Draw bottom card(s)\", function(playerColor) contextFunc(playerColor, 3) end)\n elseif id == \"05189\" or id == \"05189-t\" then\n -- mystic lvl 3: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n end\nend\n\nfunction contextFunc(playerColor, amount)\n local options = { \"Encounter Deck\" }\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playmatApi.getMatColor(color)\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n table.insert(options, color)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _, function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n -- variable initialization\n local deck = nil\n local deckSize = 1\n local deckAreaObjects = {}\n\n -- get the respective deck\n if owner == \"Encounter Deck\" then\n deck = mythosAreaApi.getEncounterDeck()\n else\n local matColor = playmatApi.getMatColor(owner)\n deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n deck = deckAreaObjects.draw\n end\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n -- get class via metadata and proceed menu accordingly:\n -- lvl 0: draw 1 card from the bottom\n -- mystic lvl 3: draw 1 card from the bottom\n local buttonLabel = \"Draw bottom card\"\n local amount = 1\n local notes = JSON.decode(self.getGMNotes())\n if notes.id == \"05188\" or notes.id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n buttonLabel = buttonLabel .. \"(s)\"\n amount = 3\n end\n self.addContextMenuItem(buttonLabel, function(playerColor) contextFunc(playerColor, amount) end)\nend\n\nfunction contextFunc(playerColor, amount)\n Player[playerColor].clearSelectedObjects()\n deckData = {}\n local options = {}\n\n -- check for encounter deck\n local encounterDeck = mythosAreaApi.getEncounterDeck()\n if encounterDeck then\n table.insert(options, \"Encounter Deck\")\n deckData[\"Encounter Deck\"] = { draw = encounterDeck }\n end\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playermatApi.getMatColor(color)\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n local playerName = Player[color].steam_name\n local invName = playermatApi.getInvestigatorName(matColor)\n\n -- figure out a proper display name for the drop down menu\n local displayName = color .. \" (color)\"\n if playerName then\n displayName = playerName\n elseif invName ~= \"\" then\n displayName = invName .. \" (inv)\"\n end\n\n -- recreate table in this script to have access\n deckData[displayName] = {\n draw = deckAreaObjects.draw,\n topCard = deckAreaObjects.topCard\n }\n table.insert(options, displayName)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _,\n function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n local deckAreaObjects = deckData[owner]\n local deck = deckAreaObjects.draw\n local deckSize = 1\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -187573,7 +131292,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- this script is shared between the lvl 0 and lvl 3 versions of Scroll of Secrets\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- get class via metadata and create context menu accordingly\nfunction onLoad()\n local notes = JSON.decode(self.getGMNotes())\n if notes then\n createContextMenu(notes.id)\n else\n print(\"Missing metadata for Scroll of Secrets!\")\n end\nend\n\nfunction createContextMenu(id)\n if id == \"05116\" or id == \"05116-t\" then\n -- lvl 0: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n elseif id == \"05188\" or id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n self.addContextMenuItem(\"Draw bottom card(s)\", function(playerColor) contextFunc(playerColor, 3) end)\n elseif id == \"05189\" or id == \"05189-t\" then\n -- mystic lvl 3: draw 1 card from the bottom\n self.addContextMenuItem(\"Draw bottom card\", function(playerColor) contextFunc(playerColor, 1) end)\n end\nend\n\nfunction contextFunc(playerColor, amount)\n local options = { \"Encounter Deck\" }\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playmatApi.getMatColor(color)\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n table.insert(options, color)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _, function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n -- variable initialization\n local deck = nil\n local deckSize = 1\n local deckAreaObjects = {}\n\n -- get the respective deck\n if owner == \"Encounter Deck\" then\n deck = mythosAreaApi.getEncounterDeck()\n else\n local matColor = playmatApi.getMatColor(owner)\n deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n deck = deckAreaObjects.draw\n end\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/cards/ScrollofSecrets\")\nend)\n__bundle_register(\"playercards/cards/ScrollofSecrets\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfunction onLoad()\n -- get class via metadata and proceed menu accordingly:\n -- lvl 0: draw 1 card from the bottom\n -- mystic lvl 3: draw 1 card from the bottom\n local buttonLabel = \"Draw bottom card\"\n local amount = 1\n local notes = JSON.decode(self.getGMNotes())\n if notes.id == \"05188\" or notes.id == \"05188-t\" then\n -- seeker lvl 3: draw 3 cards from the bottom\n buttonLabel = buttonLabel .. \"(s)\"\n amount = 3\n end\n self.addContextMenuItem(buttonLabel, function(playerColor) contextFunc(playerColor, amount) end)\nend\n\nfunction contextFunc(playerColor, amount)\n Player[playerColor].clearSelectedObjects()\n deckData = {}\n local options = {}\n\n -- check for encounter deck\n local encounterDeck = mythosAreaApi.getEncounterDeck()\n if encounterDeck then\n table.insert(options, \"Encounter Deck\")\n deckData[\"Encounter Deck\"] = { draw = encounterDeck }\n end\n\n -- check for players with a deck and only display them as option\n for _, color in ipairs(Player.getAvailableColors()) do\n local matColor = playermatApi.getMatColor(color)\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n\n if deckAreaObjects.draw or deckAreaObjects.topCard then\n local playerName = Player[color].steam_name\n local invName = playermatApi.getInvestigatorName(matColor)\n\n -- figure out a proper display name for the drop down menu\n local displayName = color .. \" (color)\"\n if playerName then\n displayName = playerName\n elseif invName ~= \"\" then\n displayName = invName .. \" (inv)\"\n end\n\n -- recreate table in this script to have access\n deckData[displayName] = {\n draw = deckAreaObjects.draw,\n topCard = deckAreaObjects.topCard\n }\n table.insert(options, displayName)\n end\n end\n\n -- show the target selection dialog\n Player[playerColor].showOptionsDialog(\"Select target deck\", options, _,\n function(owner) drawCardsFromBottom(playerColor, owner, amount) end)\nend\n\nfunction drawCardsFromBottom(playerColor, owner, amount)\n local deckAreaObjects = deckData[owner]\n local deck = deckAreaObjects.draw\n local deckSize = 1\n\n -- error handling\n if not deck then\n printToColor(\"Couldn't find deck!\", playerColor)\n return\n end\n\n -- set deck size if there is actually a deck and not just a card\n if deck.type == \"Deck\" then\n deckSize = #deck.getObjects()\n end\n\n -- proceed according to deck size\n if deckSize \u003e amount then\n for i = 1, amount do\n local card = deck.takeObject({ top = false, flip = true })\n card.deal(1, playerColor)\n end\n else\n -- deal the whole deck\n deck.deal(amount, playerColor)\n\n if deckSize \u003c amount then\n -- Norman Withers handling\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.deal(1, playerColor)\n deckSize = deckSize + 1\n end\n\n -- warning message for player\n if deckSize \u003c amount then\n printToColor(\"Deck didn't contain enough cards.\", playerColor)\n end\n end\n end\n printToColor(\"Handle the drawn cards according to the ability text on 'Scroll of Secrets'.\", playerColor)\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Card", @@ -188609,7 +132328,7 @@ }, "Description": "The Dead Speak (Advanced)", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90050\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Instrument. Relic.\",\n \"willpowerIcons\": 2,\n \"wildIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\"\n}", + "GMNotes": "{\n \"id\": \"90050\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Neutral\",\n \"cost\": 2,\n \"traits\": \"Item. Instrument. Relic.\",\n \"willpowerIcons\": 2,\n \"wildIcons\": 2,\n \"cycle\": \"Laid to Rest\"\n}", "GUID": "7dfd5f", "Grid": true, "GridProjection": false, @@ -188671,7 +132390,7 @@ }, "Description": "The Musician", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02004-p\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 39,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n },\n {\n \"90052\": 1\n },\n {\n \"90053\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Survivor cards\"\n },\n {\n \"trait\": [\n \"spell\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"ally\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n },\n \"limit\": 9,\n \"error\": \"You must have exactly 9 Ally cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02004-p\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"Laid to Rest\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 39,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n },\n {\n \"90052\": 1\n },\n {\n \"90053\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Survivor cards\"\n },\n {\n \"trait\": [\n \"spell\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"ally\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n },\n \"limit\": 9,\n \"error\": \"You must have exactly 9 Ally cards\"\n }\n ]\n}", "GUID": "72bf31", "Grid": true, "GridProjection": false, @@ -188733,7 +132452,7 @@ }, "Description": "The Musician", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02004-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Mystic or Neutral\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02004-pf\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer. Cursed.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"Laid to Rest\",\n \"extraToken\": \"Reaction\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"mystic\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 cards that are not Mystic or Neutral\"\n }\n ]\n}", "GUID": "c5fc80", "Grid": true, "GridProjection": false, @@ -188856,7 +132575,7 @@ }, "Description": "Advanced", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"90051\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Endtimes.\",\n \"weakness\": true,\n \"cycle\": \"The Dunwich Legacy\"\n}", + "GMNotes": "{\n \"id\": \"90051\",\n \"type\": \"Treachery\",\n \"class\": \"Neutral\",\n \"traits\": \"Endtimes.\",\n \"weakness\": true,\n \"cycle\": \"Laid to Rest\"\n}", "GUID": "561775", "Grid": true, "GridProjection": false, @@ -188917,7 +132636,7 @@ }, "Description": "The Musician", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"02004-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"The Dunwich Legacy\",\n \"deck_requirements\": {\n \"size\": 39,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n },\n {\n \"90052\": 1\n },\n {\n \"90053\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Survivor cards\"\n },\n {\n \"trait\": [\n \"spell\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"ally\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n },\n \"limit\": 9,\n \"error\": \"You must have exactly 9 Ally cards\"\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"02004-pb\",\n \"type\": \"Investigator\",\n \"class\": \"Mystic\",\n \"traits\": \"Performer.\",\n \"willpowerIcons\": 4,\n \"intellectIcons\": 3,\n \"combatIcons\": 3,\n \"agilityIcons\": 2,\n \"cycle\": \"Laid to Rest\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 39,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"90050\": 1,\n \"02012\": 1\n },\n {\n \"90051\": 1,\n \"02013\": 1\n },\n {\n \"90052\": 1\n },\n {\n \"90053\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"faction\": [\n \"mystic\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 3\n }\n },\n {\n \"faction\": [\n \"survivor\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 0\n },\n \"limit\": 5,\n \"error\": \"You cannot have more than 5 Survivor cards\"\n },\n {\n \"trait\": [\n \"spell\",\n \"cursed\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 4\n }\n },\n {\n \"trait\": [\n \"ally\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n },\n \"limit\": 9,\n \"error\": \"You must have exactly 9 Ally cards\"\n }\n ]\n}", "GUID": "aba863", "Grid": true, "GridProjection": false, @@ -189164,7 +132883,7 @@ }, "Description": "The Countess", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"10009\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Drifter. Socialite.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10010\": 3\n },\n {\n \"10011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"special\": [\n \"parley\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"10009\",\n \"type\": \"Investigator\",\n \"class\": \"Rogue\",\n \"traits\": \"Drifter. Socialite.\",\n \"willpowerIcons\": 3,\n \"intellectIcons\": 4,\n \"combatIcons\": 2,\n \"agilityIcons\": 4,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"extraToken\": \"Parley\",\n \"deck_requirements\": {\n \"size\": 30,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10010\": 3\n },\n {\n \"10011\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"rogue\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"special\": [\n \"parley\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n }\n ]\n}", "GUID": "54eaa5", "Grid": true, "GridProjection": false, @@ -189962,7 +133681,7 @@ }, "Description": "The Farmhand", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"10015\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Assistant. Warden.\",\n \"bonded\": [\n {\n \"count\": 1,\n \"maxCount\": 1,\n \"id\": \"10015-b1\"\n },\n {\n \"count\": 1,\n \"maxCount\": 1,\n \"id\": \"10015-b2\"\n }\n ],\n \"willpowerIcons\": 3,\n \"intellectIcons\": 1,\n \"combatIcons\": 5,\n \"agilityIcons\": 3,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10017\": 1\n },\n {\n \"10018\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"insight\",\n \"spirit\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n },\n \"limit\": 10\n }\n ]\n}", + "GMNotes": "{\n \"id\": \"10015\",\n \"type\": \"Investigator\",\n \"class\": \"Survivor\",\n \"traits\": \"Assistant. Warden.\",\n \"bonded\": [\n {\n \"count\": 1,\n \"maxCount\": 1,\n \"id\": \"10015-b1\"\n },\n {\n \"count\": 1,\n \"maxCount\": 1,\n \"id\": \"10015-b2\"\n }\n ],\n \"willpowerIcons\": 3,\n \"intellectIcons\": 1,\n \"combatIcons\": 5,\n \"agilityIcons\": 3,\n \"cycle\": \"The Feast of Hemlock Vale\",\n \"extraToken\": \"None\",\n \"deck_requirements\": {\n \"size\": 35,\n \"randomBasicWeaknessCount\": 1,\n \"signatures\": [\n {\n \"10017\": 1\n },\n {\n \"10018\": 1\n }\n ]\n },\n \"deck_options\": [\n {\n \"faction\": [\n \"seeker\",\n \"neutral\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 5\n }\n },\n {\n \"trait\": [\n \"insight\",\n \"spirit\"\n ],\n \"level\": {\n \"min\": 0,\n \"max\": 2\n },\n \"limit\": 10\n }\n ]\n}", "GUID": "3764cd", "Grid": true, "GridProjection": false, @@ -192114,7 +135833,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"10129\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 0,\n \"level\": 0,\n \"traits\": \"Double.\",\n \"wildIcons\": 1,\n \"cycle\": \"The Feast of Hemlock Vale\"\n}", + "GMNotes": "{\n \"id\": \"10129\",\n \"type\": \"Event\",\n \"class\": \"Neutral\",\n \"cost\": 0,\n \"level\": 0,\n \"traits\": \"Double.\",\n \"wildIcons\": 1,\n \"uses\": [\n {\n \"count\": 2,\n \"type\": \"Universal\",\n \"token\": \"universalActionAbility\"\n }\n ],\n \"cycle\": \"The Feast of Hemlock Vale\"\n}", "GUID": "24d3b3", "Grid": true, "GridProjection": false, @@ -193714,7 +137433,7 @@ }, "Description": "", "DragSelectable": true, - "GMNotes": "{\n \"id\": \"10082\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Rogue\",\n \"cost\": 2,\n \"level\": 4,\n \"traits\": \"Item. Illicit.\",\n \"intellectIcons\": 1,\n \"agilityIcons\": 1,\n \"cycle\": \"The Feast of Hemlock Vale\"\n}", + "GMNotes": "{\n \"id\": \"10082\",\n \"type\": \"Asset\",\n \"slot\": \"Hand\",\n \"class\": \"Rogue\",\n \"cost\": 2,\n \"level\": 4,\n \"traits\": \"Item. Illicit.\",\n \"intellectIcons\": 1,\n \"agilityIcons\": 1,\n \"uses\": [\n {\n \"count\": 0,\n \"type\": \"Suspicion\",\n \"token\": \"resource\"\n }\n ],\n \"cycle\": \"The Feast of Hemlock Vale\"\n}", "GUID": "7ebb67", "Grid": true, "GridProjection": false, @@ -194656,7 +138375,7 @@ "Snap": true, "Sticky": true, "Tags": [ - "ScenarioCard" + "PlayerCard" ], "Tooltip": true, "Transform": { @@ -195426,7 +139145,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/AllCardsBag\")\nend)\n__bundle_register(\"playercards/AllCardsBag\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal cardIdIndex = { }\nlocal classAndLevelIndex = { }\nlocal basicWeaknessList = { }\nlocal uniqueWeaknessList = { }\nlocal cycleIndex = { }\n\nlocal indexingDone = false\n\nfunction onLoad()\n self.addContextMenuItem(\"Rebuild Index\", startIndexBuild)\n math.randomseed(os.time())\n Wait.frames(startIndexBuild, 30)\nend\n\n-- Called by Hotfix bags when they load. If we are still loading indexes, then\n-- the all cards and hotfix bags are being loaded together, and we can ignore\n-- this call as the hotfix will be included in the initial indexing. If it is\n-- called once indexing is complete it means the hotfix bag has been added\n-- later, and we should rebuild the index to integrate the hotfix bag.\nfunction rebuildIndexForHotfix()\n if (indexingDone) then\n startIndexBuild()\n end\nend\n\n-- Resets all current bag indexes\nfunction clearIndexes()\n indexingDone = false\n cardIdIndex = { }\n classAndLevelIndex = { }\n classAndLevelIndex[\"Guardian-upgrade\"] = { }\n classAndLevelIndex[\"Seeker-upgrade\"] = { }\n classAndLevelIndex[\"Mystic-upgrade\"] = { }\n classAndLevelIndex[\"Survivor-upgrade\"] = { }\n classAndLevelIndex[\"Rogue-upgrade\"] = { }\n classAndLevelIndex[\"Neutral-upgrade\"] = { }\n classAndLevelIndex[\"Guardian-level0\"] = { }\n classAndLevelIndex[\"Seeker-level0\"] = { }\n classAndLevelIndex[\"Mystic-level0\"] = { }\n classAndLevelIndex[\"Survivor-level0\"] = { }\n classAndLevelIndex[\"Rogue-level0\"] = { }\n classAndLevelIndex[\"Neutral-level0\"] = { }\n cycleIndex = { }\n basicWeaknessList = { }\n uniqueWeaknessList = { }\nend\n\n-- Clears the bag indexes and starts the coroutine to rebuild the indexes\nfunction startIndexBuild()\n clearIndexes()\n startLuaCoroutine(self, \"buildIndex\")\nend\n\nfunction onObjectLeaveContainer(container, _)\n if container == self then\n broadcastToAll(\"Removing cards from the All Player Cards bag may break some functions.\", \"Red\")\n end\nend\n\n-- Create the card indexes by iterating all cards in the bag, parsing their metadata\n-- and creating the keyed lookup tables for the cards. This is a coroutine which will\n-- spread the workload by processing 20 cards before yielding.\nfunction buildIndex()\n local cardCount = 0\n indexingDone = false\n\n -- process the allcardsbag itself\n for _, cardData in ipairs(self.getData().ContainedObjects) do\n addCardToIndex(cardData)\n cardCount = cardCount + 1\n if cardCount \u003e 19 then\n cardCount = 0\n coroutine.yield(0)\n end\n end\n\n -- process hotfix bags (and the additional playercards bag)\n for _, hotfixBag in ipairs(getObjectsWithTag(\"AllCardsHotfix\")) do\n local hotfixData = hotfixBag.getData()\n if not hotfixData.ContainedObjects then break end\n\n for _, cardData in ipairs(hotfixData.ContainedObjects) do\n -- process containers\n if cardData.ContainedObjects then\n for _, deepCardData in ipairs(cardData.ContainedObjects) do\n addCardToIndex(deepCardData)\n cardCount = cardCount + 1\n if cardCount \u003e 19 then\n cardCount = 0\n coroutine.yield(0)\n end\n end\n -- process single cards\n else\n addCardToIndex(cardData)\n cardCount = cardCount + 1\n if cardCount \u003e 19 then\n cardCount = 0\n coroutine.yield(0)\n end\n end\n end\n end\n\n buildSupplementalIndexes()\n indexingDone = true\n return 1\nend\n\n-- Adds a card to any indexes it should be a part of, based on its metadata\n---@param cardData table TTS object data for the card\nfunction addCardToIndex(cardData)\n -- using the more efficient 'json.parse()' to speed this process up\n local cardMetadata = json.parse(cardData.GMNotes)\n if not cardMetadata then return end\n\n -- use the ZoopGuid as fallback if no id present\n cardIdIndex[cardMetadata.id or cardMetadata.TtsZoopGuid] = { data = cardData, metadata = cardMetadata }\n\n -- also add data for alternate ids\n if cardMetadata.alternate_ids ~= nil then\n for _, alternateId in ipairs(cardMetadata.alternate_ids) do\n cardIdIndex[alternateId] = { data = cardData, metadata = cardMetadata }\n end\n end\nend\n\nfunction buildSupplementalIndexes()\n for cardId, card in pairs(cardIdIndex) do\n local cardMetadata = card.metadata\n -- If the ID key and the metadata ID don't match this is a duplicate card created by an alternate_id, and we should skip it\n if cardId == cardMetadata.id then\n -- Add card to the basic weakness list, if appropriate. Some weaknesses have multiple copies, and are added multiple times\n if cardMetadata.weakness then\n table.insert(uniqueWeaknessList, cardMetadata.id)\n if cardMetadata.basicWeaknessCount ~= nil then\n for i = 1, cardMetadata.basicWeaknessCount do\n table.insert(basicWeaknessList, cardMetadata.id)\n end\n end\n end\n\n -- Excludes signature cards (which have no class or level)\n if cardMetadata.class ~= nil and cardMetadata.level ~= nil then\n local upgradeKey\n if cardMetadata.level \u003e 0 then\n upgradeKey = \"-upgrade\"\n else\n upgradeKey = \"-level0\"\n end\n\n -- parse classes (separated by \"|\") and add the card to the appropriate class and level indices\n for str in cardMetadata.class:gmatch(\"([^|]+)\") do\n table.insert(classAndLevelIndex[str .. upgradeKey], cardMetadata.id)\n end\n\n -- add to cycle index\n local cycleName = cardMetadata.cycle\n if cycleName ~= nil then\n cycleName = string.lower(cycleName)\n\n -- remove \"return to \" from cycle names\n cycleName = cycleName:gsub(\"return to \", \"\")\n\n -- override cycle name for night of the zealot\n cycleName = cycleName:gsub(\"the night of the zealot\", \"core\")\n\n if cycleIndex[cycleName] == nil then\n cycleIndex[cycleName] = { }\n end\n table.insert(cycleIndex[cycleName], cardMetadata.id)\n end\n end\n end\n end\n\n -- sort class and level indices\n for _, indexTable in pairs(classAndLevelIndex) do\n table.sort(indexTable, cardComparator)\n end\n\n -- sort cycle indices\n for _, indexTable in pairs(cycleIndex) do\n table.sort(indexTable)\n end\n\n -- sort weakness indices\n table.sort(basicWeaknessList, cardComparator)\n table.sort(uniqueWeaknessList, cardComparator)\nend\n\n-- Comparison function used to sort the class card bag indexes. Sorts by card level, then name, then subname.\nfunction cardComparator(id1, id2)\n local card1 = cardIdIndex[id1]\n local card2 = cardIdIndex[id2]\n\n if card1.metadata.level ~= card2.metadata.level then\n return card1.metadata.level \u003c card2.metadata.level\n elseif card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n else\n return card1.data.Description \u003c card2.data.Description\n end\nend\n\nfunction isIndexReady()\n return indexingDone\nend\n\n-- Returns a specific card from the bag, based on ArkhamDB ID\n-- Params table:\n-- id: String ID of the card to retrieve\n-- Return: If the indexes are still being constructed, an empty table is\n-- returned. Otherwise, a single table with the following fields\n-- cardData: TTS object data, suitable for spawning the card\n-- cardMetadata: Table of parsed metadata\nfunction getCardById(params)\n if (not indexingDone) then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", {0.9, 0.2, 0.2})\n return { }\n end\n return cardIdIndex[params.id]\nend\n\n-- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n-- Params table:\n-- class: String class to retrieve (\"Guardian\", \"Seeker\", etc)\n-- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0\n-- Return: If the indexes are still being constructed, returns an empty table.\n-- Otherwise, a list of tables, each with the following fields\n-- cardData: TTS object data, suitable for spawning the card\n-- cardMetadata: Table of parsed metadata\nfunction getCardsByClassAndLevel(params)\n if (not indexingDone) then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", {0.9, 0.2, 0.2})\n return { }\n end\n local upgradeKey\n if (params.upgraded) then\n upgradeKey = \"-upgrade\"\n else\n upgradeKey = \"-level0\"\n end\n return classAndLevelIndex[params.class..upgradeKey];\nend\n\nfunction getCardsByCycle(cycleName)\n if (not indexingDone) then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", {0.9, 0.2, 0.2})\n return { }\n end\n return cycleIndex[string.lower(cycleName)]\nend\n\n-- Searches the bag for cards which match the given name and returns a list. Note that this is\n-- an O(n) search without index support. It may be slow.\n-- Parameter array must contain these fields to define the search:\n-- name String or string fragment to search for names\n-- exact Whether the name match should be exact\nfunction getCardsByName(params)\n local name = params.name\n local exact = params.exact\n local results = { }\n -- Track cards (by ID) that we've added to avoid duplicates that may come from alternate IDs\n local addedCards = { }\n for _, cardData in pairs(cardIdIndex) do\n if (not addedCards[cardData.metadata.id]) then\n if (exact and (string.lower(cardData.data.Nickname) == string.lower(name)))\n or (not exact and string.find(string.lower(cardData.data.Nickname), string.lower(name), 1, true)) then\n table.insert(results, cardData)\n addedCards[cardData.metadata.id] = true\n end\n end\n end\n return results\nend\n\n-- Gets a random basic weakness from the bag. Once a given ID has been returned\n-- it will be removed from the list and cannot be selected again until a reload\n-- occurs or the indexes are rebuilt, which will refresh the list to include all\n-- weaknesses.\n-- Return: String ID of the selected weakness.\nfunction getRandomWeaknessId()\n local availableWeaknesses = buildAvailableWeaknesses()\n if (#availableWeaknesses \u003e 0) then\n return availableWeaknesses[math.random(#availableWeaknesses)]\n end\nend\n\n-- Constructs a list of available basic weaknesses by starting with the full pool of basic\n-- weaknesses then removing any which are currently in the play or deck construction areas\n-- Return: Table array of weakness IDs which are valid to choose from\nfunction buildAvailableWeaknesses()\n local weaknessesInPlay = { }\n local allObjects = getAllObjects()\n for _, object in ipairs(allObjects) do\n if (object.name == \"Deck\") then\n for _, cardData in ipairs(object.getData().ContainedObjects) do\n local cardMetadata = JSON.decode(cardData.GMNotes)\n incrementWeaknessCount(weaknessesInPlay, cardMetadata)\n end\n elseif (object.name == \"Card\") then\n local cardMetadata = JSON.decode(object.getGMNotes())\n incrementWeaknessCount(weaknessesInPlay, cardMetadata)\n end\n end\n\n local availableWeaknesses = { }\n for _, weaknessId in ipairs(basicWeaknessList) do\n if (weaknessesInPlay[weaknessId] ~= nil and weaknessesInPlay[weaknessId] \u003e 0) then\n weaknessesInPlay[weaknessId] = weaknessesInPlay[weaknessId] - 1\n else\n table.insert(availableWeaknesses, weaknessId)\n end\n end\n return availableWeaknesses\nend\n\nfunction getBasicWeaknesses()\n return basicWeaknessList\nend\n\nfunction getUniqueWeaknesses()\n return uniqueWeaknessList\nend\n\n-- Helper function that adds one to the table entry for the number of weaknesses in play\nfunction incrementWeaknessCount(table, cardMetadata)\n if (isBasicWeakness(cardMetadata)) then\n if (table[cardMetadata.id] == nil) then\n table[cardMetadata.id] = 1\n else\n table[cardMetadata.id] = table[cardMetadata.id] + 1\n end\n end\nend\n\nfunction isBasicWeakness(cardMetadata)\n return cardMetadata ~= nil\n and cardMetadata.weakness\n and cardMetadata.basicWeaknessCount ~= nil\n and cardMetadata.basicWeaknessCount \u003e 0\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/AllCardsBag\")\nend)\n__bundle_register(\"playercards/AllCardsBag\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\nlocal cardIdIndex = {}\nlocal classAndLevelIndex = {}\nlocal basicWeaknessList = {}\nlocal uniqueWeaknessList = {}\nlocal cycleIndex = {}\n\nlocal indexingDone = false\nlocal otherCardsDetected = false\n\nfunction onLoad()\n self.addContextMenuItem(\"Rebuild Index\", startIndexBuild)\n math.randomseed(os.time())\n Wait.frames(startIndexBuild, 30)\nend\n\n-- Called by Hotfix bags when they load. If we are still loading indexes, then\n-- the all cards and hotfix bags are being loaded together, and we can ignore\n-- this call as the hotfix will be included in the initial indexing. If it is\n-- called once indexing is complete it means the hotfix bag has been added\n-- later, and we should rebuild the index to integrate the hotfix bag.\nfunction rebuildIndexForHotfix()\n if indexingDone then\n startIndexBuild()\n end\nend\n\n-- Resets all current bag indexes\nfunction clearIndexes()\n indexingDone = false\n cardIdIndex = {}\n classAndLevelIndex = {}\n classAndLevelIndex[\"Guardian-upgrade\"] = {}\n classAndLevelIndex[\"Seeker-upgrade\"] = {}\n classAndLevelIndex[\"Mystic-upgrade\"] = {}\n classAndLevelIndex[\"Survivor-upgrade\"] = {}\n classAndLevelIndex[\"Rogue-upgrade\"] = {}\n classAndLevelIndex[\"Neutral-upgrade\"] = {}\n classAndLevelIndex[\"Guardian-level0\"] = {}\n classAndLevelIndex[\"Seeker-level0\"] = {}\n classAndLevelIndex[\"Mystic-level0\"] = {}\n classAndLevelIndex[\"Survivor-level0\"] = {}\n classAndLevelIndex[\"Rogue-level0\"] = {}\n classAndLevelIndex[\"Neutral-level0\"] = {}\n cycleIndex = {}\n basicWeaknessList = {}\n uniqueWeaknessList = {}\nend\n\n-- Clears the bag indexes and starts the coroutine to rebuild the indexes\nfunction startIndexBuild()\n clearIndexes()\n startLuaCoroutine(self, \"buildIndex\")\nend\n\nfunction onObjectLeaveContainer(container, _)\n if container == self then\n broadcastToAll(\"Removing cards from the All Player Cards bag may break some functions.\", \"Red\")\n end\nend\n\n-- Create the card indexes by iterating all cards in the bag, parsing their metadata\n-- and creating the keyed lookup tables for the cards. This is a coroutine which will\n-- spread the workload by processing 20 cards before yielding.\nfunction buildIndex()\n local cardCount = 0\n indexingDone = false\n otherCardsDetected = false\n\n -- process the allcardsbag itself\n for _, cardData in ipairs(self.getData().ContainedObjects) do\n addCardToIndex(cardData)\n cardCount = cardCount + 1\n if cardCount \u003e 19 then\n cardCount = 0\n coroutine.yield(0)\n end\n end\n\n -- process hotfix bags (and the additional playercards bag)\n for _, hotfixBag in ipairs(getObjectsWithTag(\"AllCardsHotfix\")) do\n local hotfixData = hotfixBag.getData()\n\n -- if the bag is empty, continue with the next bag\n if not hotfixData.ContainedObjects then\n goto nextBag\n end\n\n for _, cardData in ipairs(hotfixData.ContainedObjects) do\n if cardData.ContainedObjects then\n -- process containers\n for _, deepCardData in ipairs(cardData.ContainedObjects) do\n addCardToIndex(deepCardData)\n cardCount = cardCount + 1\n if cardCount \u003e 19 then\n cardCount = 0\n coroutine.yield(0)\n end\n end\n else\n -- process single cards\n addCardToIndex(cardData)\n cardCount = cardCount + 1\n if cardCount \u003e 19 then\n cardCount = 0\n coroutine.yield(0)\n end\n end\n end\n ::nextBag::\n end\n\n buildSupplementalIndexes()\n updatePlayerCardPanel()\n indexingDone = true\n return 1\nend\n\n-- Adds a card to any indexes it should be a part of, based on its metadata\n---@param cardData table TTS object data for the card\nfunction addCardToIndex(cardData)\n -- using the more efficient 'json.parse()' to speed this process up\n local status, cardMetadata = pcall(function() return json.parse(cardData.GMNotes) end)\n\n -- if an error happens, fallback to the regular parser\n if status ~= true or cardMetadata == nil then\n log(\"Fast parser failed for \" .. cardData.Nickname .. \", using old parser instead.\")\n cardMetadata = JSON.decode(cardData.GMNotes)\n end\n\n -- if metadata was not valid JSON or empty, don't add the card\n if not cardMetadata == nil then\n log(\"Error parsing \" .. cardData.Nickname)\n return\n end\n\n -- use the ZoopGuid as fallback if no id present\n cardMetadata.id = cardMetadata.id or cardMetadata.TtsZoopGuid\n cardIdIndex[cardMetadata.id] = { data = cardData, metadata = cardMetadata }\n\n -- also add data for alternate ids\n if cardMetadata.alternate_ids ~= nil then\n for _, alternateId in ipairs(cardMetadata.alternate_ids) do\n cardIdIndex[alternateId] = { data = cardData, metadata = cardMetadata }\n end\n end\nend\n\n-- Creates the supplemental indexes for classes, weaknesses etc.\nfunction buildSupplementalIndexes()\n for cardId, card in pairs(cardIdIndex) do\n -- If the ID key and the metadata ID don't match this is a duplicate card created by an alternate_id, and we should skip it\n if cardId == card.metadata.id then\n -- Add card to the basic weakness list, if appropriate. Some weaknesses have multiple copies, and are added multiple times\n if card.metadata.weakness then\n table.insert(uniqueWeaknessList, card.metadata.id)\n if card.metadata.basicWeaknessCount ~= nil then\n for i = 1, card.metadata.basicWeaknessCount do\n table.insert(basicWeaknessList, card.metadata.id)\n end\n end\n end\n\n -- Excludes signature cards (which have no class or level)\n if card.metadata.class ~= nil and card.metadata.level ~= nil then\n local upgradeKey = \"-level0\"\n if card.metadata.level \u003e 0 then\n upgradeKey = \"-upgrade\"\n end\n\n -- parse classes (separated by \"|\") and add the card to the appropriate class and level indices\n for str in card.metadata.class:gmatch(\"([^|]+)\") do\n table.insert(classAndLevelIndex[str .. upgradeKey], card.metadata.id)\n end\n\n -- add to cycle index\n local cycleName = card.metadata.cycle\n if cycleName ~= nil then\n cycleName = string.lower(cycleName)\n\n -- remove \"return to \" from cycle names\n cycleName = cycleName:gsub(\"return to \", \"\")\n\n -- override cycle name for night of the zealot\n cycleName = cycleName:gsub(\"the night of the zealot\", \"core\")\n else\n -- track cards without defined cycle (should only be fan-made cards)\n cycleName = \"other\"\n otherCardsDetected = true\n end\n\n -- maybe initialize table\n if cycleIndex[cycleName] == nil then\n cycleIndex[cycleName] = {}\n end\n table.insert(cycleIndex[cycleName], card.metadata.id)\n end\n end\n end\n\n -- sort class and level indices\n for _, indexTable in pairs(classAndLevelIndex) do\n table.sort(indexTable, cardComparator)\n end\n\n -- sort cycle indices\n for _, indexTable in pairs(cycleIndex) do\n table.sort(indexTable)\n end\n\n -- sort weakness indices\n table.sort(basicWeaknessList, cardComparator)\n table.sort(uniqueWeaknessList, cardComparator)\nend\n\n-- Comparison function used to sort the class card bag indexes. Sorts by card level, then name, then subname.\nfunction cardComparator(id1, id2)\n local card1 = cardIdIndex[id1]\n local card2 = cardIdIndex[id2]\n\n if card1.metadata.level ~= card2.metadata.level then\n return card1.metadata.level \u003c card2.metadata.level\n elseif card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n else\n return card1.data.Description \u003c card2.data.Description\n end\nend\n\n-- inform the player card panel about the presence of other cards (no cycle -\u003e fan-made)\nfunction updatePlayerCardPanel()\n local panel = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayerCardPanel\")\n panel.call(\"createXML\", otherCardsDetected)\nend\n\n---@return boolean: If true, the bag is currently not indexing and ready to be accessed\nfunction isIndexReady()\n if not indexingDone then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", { 0.9, 0.2, 0.2 })\n end\n return indexingDone\nend\n\n-- Returns a specific card from the bag, based on ArkhamDB ID\n---@param params table ID of the card to retrieve\n---@return table: If the indexes are still being constructed, returns an empty table.\n-- Otherwise, a single table with the following fields\n-- data: TTS object data, suitable for spawning the card\n-- metadata: Table of parsed metadata\nfunction getCardById(params)\n if not isIndexReady() then return {} end\n return cardIdIndex[params.id]\nend\n\n-- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n---@param params table\n-- class: String class to retrieve (\"Guardian\", \"Seeker\", etc)\n-- isUpgraded: true for upgraded cards (Level 1-5), false for Level 0\n---@return table: If the indexes are still being constructed, returns an empty table.\n-- Otherwise, a list of tables, each with the following fields\n-- data: TTS object data, suitable for spawning the card\n-- metadata: Table of parsed metadata\nfunction getCardsByClassAndLevel(params)\n if not isIndexReady() then return {} end\n\n local upgradeKey = \"-level0\"\n if params.upgraded then\n upgradeKey = \"-upgrade\"\n end\n return classAndLevelIndex[params.class .. upgradeKey]\nend\n\n-- Returns a list of cards from the bag matching a cycle\n---@param params table\n-- cycle: String cycle to retrieve (\"The Scarlet Keys\" etc.)\n-- sortByMetadata: true to sort the table by metadata instead of ID\n---@return table: If the indexes are still being constructed, returns an empty table.\n-- Otherwise, a list of tables, each with the following fields\n-- data: TTS object data, suitable for spawning the card\n-- metadata: Table of parsed metadata\nfunction getCardsByCycle(params)\n if not isIndexReady() then return {} end\n\n if not params.sortByMetadata then\n return cycleIndex[string.lower(params.cycle)]\n end\n\n -- sort list by metadata (useful for custom cards without proper IDs)\n local cardList = {}\n for _, id in ipairs(cycleIndex[string.lower(params.cycle)]) do\n table.insert(cardList, id)\n end\n\n table.sort(cardList, metadataSortFunction)\n return cardList\nend\n\n-- sorts cards by metadata: class, type, level, name and then description\nfunction metadataSortFunction(id1, id2)\n local card1 = cardIdIndex[id1]\n local card2 = cardIdIndex[id2]\n\n -- extract class per card\n local classValue1 = getClassValueFromString(card1.metadata.class)\n local classValue2 = getClassValueFromString(card2.metadata.class)\n\n -- conversion tables to simplify type sorting\n local typeConversion = {\n Asset = 1,\n Event = 2,\n Skill = 3\n }\n\n if classValue1 ~= classValue2 then\n return classValue1 \u003c classValue2\n elseif typeConversion[card1.metadata.type] ~= typeConversion[card2.metadata.type] then\n return typeConversion[card1.metadata.type] \u003c typeConversion[card2.metadata.type]\n elseif card1.metadata.level ~= card2.metadata.level then\n return card1.metadata.level \u003c card2.metadata.level\n elseif card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n else\n return card1.data.Description \u003c card2.data.Description\n end\nend\n\n-- helper function to calculate the class value for sorting from the \"|\" separated string\nfunction getClassValueFromString(s)\n local classValueList = {\n Guardian = 1,\n Seeker = 2,\n Rogue = 3,\n Mystic = 4,\n Survivor = 5,\n Neutral = 6\n }\n local classValue = 0\n for str in s:gmatch(\"([^|]+)\") do\n -- this sorts multiclass cards\n classValue = classValue * 10 + classValueList[str]\n end\n return classValue\nend\n\n-- Searches the bag for cards which match the given name and returns a list. Note that this is\n-- an O(n) search without index support. It may be slow.\n-- Parameter array must contain these fields to define the search:\n-- name: String or string fragment to search for names\n-- exact: Whether the name match should be exact\nfunction getCardsByName(params)\n local name = params.name\n local exact = params.exact\n local results = {}\n\n -- Track cards (by ID) that we've added to avoid duplicates that may come from alternate IDs\n local addedCards = {}\n for _, cardData in pairs(cardIdIndex) do\n if (not addedCards[cardData.metadata.id]) then\n if (exact and (string.lower(cardData.data.Nickname) == string.lower(name)))\n or (not exact and string.find(string.lower(cardData.data.Nickname), string.lower(name), 1, true)) then\n table.insert(results, cardData)\n addedCards[cardData.metadata.id] = true\n end\n end\n end\n return results\nend\n\n-- Gets a random basic weakness from the bag. Once a given ID has been returned it will be\n-- removed from the list and cannot be selected again until a reload occurs or the indexes\n-- are rebuilt, which will refresh the list to include all weaknesses.\n---@return string: ID of the selected weakness\nfunction getRandomWeaknessId()\n local availableWeaknesses = buildAvailableWeaknesses()\n if #availableWeaknesses \u003e 0 then\n return availableWeaknesses[math.random(#availableWeaknesses)]\n end\nend\n\n-- Constructs a list of available basic weaknesses by starting with the full pool of basic\n-- weaknesses then removing any which are currently in the play or deck construction areas\n---@param traits? string Trait(s) to use as filter\n---@return table: Array of weakness IDs which are valid to choose from\nfunction buildAvailableWeaknesses(traits)\n local weaknessesInPlay = {}\n local allObjects = getAllObjects()\n for _, object in ipairs(allObjects) do\n if object.type == \"Deck\" then\n for _, cardData in ipairs(object.getData().ContainedObjects) do\n incrementWeaknessCount(weaknessesInPlay, JSON.decode(cardData.GMNotes))\n end\n elseif object.type == \"Card\" then\n incrementWeaknessCount(weaknessesInPlay, JSON.decode(object.getGMNotes()))\n end\n end\n\n local availableWeaknesses = {}\n for _, weaknessId in ipairs(basicWeaknessList) do\n if (weaknessesInPlay[weaknessId] ~= nil and weaknessesInPlay[weaknessId] \u003e 0) then\n weaknessesInPlay[weaknessId] = weaknessesInPlay[weaknessId] - 1\n else\n if traits then\n -- split the string into separate traits (separated by \"|\")\n local allowedTraits = {}\n for str in traits:gmatch(\"([^|]+)\") do\n -- remove dots\n str = str:gsub(\"[%.]\", \"\")\n\n -- remove leading and trailing whitespace\n str = str:match(\"^%s*(.-)%s*$\")\n\n -- make sure string ends with a dot\n str = string.lower(str .. \".\")\n table.insert(allowedTraits, str)\n end\n\n -- make sure the trait is present on the weakness\n local card = cardIdIndex[weaknessId]\n for _, allowedTrait in ipairs(allowedTraits) do\n if string.contains(string.lower(card.metadata.traits), allowedTrait) then\n table.insert(availableWeaknesses, weaknessId)\n break\n end\n end\n else\n table.insert(availableWeaknesses, weaknessId)\n end\n end\n end\n return availableWeaknesses\nend\n\nfunction getBasicWeaknesses()\n return basicWeaknessList\nend\n\nfunction getUniqueWeaknesses()\n return uniqueWeaknessList\nend\n\n-- Helper function that adds one to the table entry for the number of weaknesses in play\nfunction incrementWeaknessCount(table, cardMetadata)\n if isBasicWeakness(cardMetadata) then\n if table[cardMetadata.id] == nil then\n table[cardMetadata.id] = 1\n else\n table[cardMetadata.id] = table[cardMetadata.id] + 1\n end\n end\nend\n\nfunction isBasicWeakness(cardMetadata)\n return cardMetadata ~= nil\n and cardMetadata.weakness\n and cardMetadata.basicWeaknessCount ~= nil\n and cardMetadata.basicWeaknessCount \u003e 0\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MaterialIndex": -1, "MeasureMovement": false, @@ -195485,7 +139204,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playermat/InvestigatorSkillTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.height = 650\nbuttonParameters.width = 700\nbuttonParameters.position = { x = -4.775, y = 0.1, z = -0.03 }\nbuttonParameters.color = { 0, 0, 0, 0 }\nbuttonParameters.font_color = { 0, 0, 0, 100 }\nbuttonParameters.font_size = 450\n\nfunction onSave() return JSON.encode(stats) end\n\n-- load stats and make buttons (left to right)\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n stats = JSON.decode(savedData) or { 1, 1, 1, 1 }\n end\n\n for index = 1, 4 do\n local fnName = \"buttonClick\" .. index\n _G[fnName] = function(_, _, isRightClick) buttonClick(isRightClick, index) end\n buttonParameters.click_function = fnName\n buttonParameters.position.x = buttonParameters.position.x + 1.91\n self.createButton(buttonParameters)\n updateButtonLabel(index)\n end\n\n self.addContextMenuItem(\"Reset to 1s\", function() updateStats({ 1, 1, 1, 1 }) end)\nend\n\nfunction buttonClick(isRightClick, index)\n stats[index] = math.min(math.max(stats[index] + (isRightClick and -1 or 1), 0), 99)\n updateButtonLabel(index)\nend\n\n-- sync the button label to the internal value\nfunction updateButtonLabel(index)\n local fontSize = buttonParameters.font_size\n local whitespace = \" \"\n\n if stats[index] \u003e 9 then\n fontSize = buttonParameters.font_size * 0.65\n whitespace = \" \"\n end\n\n self.editButton({ index = index - 1, label = stats[index] .. whitespace, font_size = fontSize })\nend\n\n-- update the stats to the provided values\n---@param newStats table Contains the new values for the stats: {Willpower, Intellect, Fight, Agility}\nfunction updateStats(newStats)\n if newStats and #newStats == 4 then\n stats = newStats\n\n for i = 1, 4 do updateButtonLabel(i) end\n elseif newStats then\n printToAll(\"Provided new stats are incomplete or incorrectly formatted.\", \"Red\")\n end\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/InvestigatorSkillTracker\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playermat/InvestigatorSkillTracker\")\nend)\n__bundle_register(\"playermat/InvestigatorSkillTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.height = 650\nbuttonParameters.width = 700\nbuttonParameters.position = { x = -4.775, y = 0.1, z = -0.03 }\nbuttonParameters.color = { 0, 0, 0, 0 }\nbuttonParameters.font_color = { 0, 0, 0, 100 }\nbuttonParameters.font_size = 450\n\nfunction onSave() return JSON.encode(stats) end\n\n-- load stats and make buttons (left to right)\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n stats = JSON.decode(savedData) or { 1, 1, 1, 1 }\n end\n\n for index = 1, 4 do\n local fnName = \"buttonClick\" .. index\n _G[fnName] = function(_, _, isRightClick) buttonClick(isRightClick, index) end\n buttonParameters.click_function = fnName\n buttonParameters.position.x = buttonParameters.position.x + 1.91\n self.createButton(buttonParameters)\n updateButtonLabel(index)\n end\n\n self.addContextMenuItem(\"Reset to 1s\", function() updateStats({ 1, 1, 1, 1 }) end)\nend\n\nfunction buttonClick(isRightClick, index)\n stats[index] = math.min(math.max(stats[index] + (isRightClick and -1 or 1), 0), 99)\n updateButtonLabel(index)\nend\n\n-- sync the button label to the internal value\nfunction updateButtonLabel(index)\n local fontSize = buttonParameters.font_size\n local whitespace = \" \"\n\n if stats[index] \u003e 9 then\n fontSize = buttonParameters.font_size * 0.65\n whitespace = \" \"\n end\n\n self.editButton({ index = index - 1, label = stats[index] .. whitespace, font_size = fontSize })\nend\n\n-- update the stats to the provided values\n---@param newStats table Contains the new values for the stats: {Willpower, Intellect, Fight, Agility}\nfunction updateStats(newStats)\n if newStats and #newStats == 4 then\n stats = newStats\n\n for i = 1, 4 do updateButtonLabel(i) end\n elseif newStats then\n printToAll(\"Provided new stats are incomplete or incorrectly formatted.\", \"Red\")\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[1,1,1,1]", "MeasureMovement": false, "Name": "Custom_Token", @@ -195709,7 +139428,7 @@ }, "CustomMesh": { "CastShadows": true, - "ColliderURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_COL.obj", + "ColliderURL": "", "Convex": true, "CustomShader": { "FresnelStrength": 0, @@ -195738,7 +139457,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", @@ -195751,7 +139470,7 @@ "Tooltip": true, "Transform": { "posX": 20.6, - "posY": 1.486, + "posY": 1.481, "posZ": -78, "rotX": 0, "rotY": 270, @@ -195771,9 +139490,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -195798,7 +139517,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardSearch\")\nend)\n__bundle_register(\"playercards/CardSearch\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/PlayerCardSpawner\")\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n\nlocal BUTTON_LABELS = {}\nBUTTON_LABELS[\"spawn\"] = {}\nBUTTON_LABELS[\"spawn\"][true] = \"All matching cards\"\nBUTTON_LABELS[\"spawn\"][false] = \"First matching card\"\nBUTTON_LABELS[\"search\"] = {}\nBUTTON_LABELS[\"search\"][true] = \"Name equals search term\"\nBUTTON_LABELS[\"search\"][false] = \"Name contains search term\"\n\nlocal inputParameters = {}\ninputParameters.label = \"Click to enter card name\"\ninputParameters.input_function = \"input_func\"\ninputParameters.function_owner = self\ninputParameters.alignment = 2\ninputParameters.position = { x = 0, y = 0.1, z = -0.62 }\ninputParameters.width = 3750\ninputParameters.height = 380\ninputParameters.font_size = 350\ninputParameters.scale = { 0.1, 1, 0.1 }\ninputParameters.color = { 0.9, 0.7, 0.5 }\ninputParameters.font_color = { 0, 0, 0 }\n\nfunction onSave() return JSON.encode({ spawnAll, searchExact, inputParameters.value }) end\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData)\n spawnAll = loadedData[1] or false\n searchExact = loadedData[2] or false\n inputParameters.value = loadedData[3] or \"\"\n self.createInput(inputParameters)\n\n -- shared parameters\n local buttonParameters = {}\n buttonParameters.function_owner = self\n buttonParameters.font_size = 180\n buttonParameters.scale = { 0.1, 1, 0.1 }\n buttonParameters.hover_color = { 0.4, 0.6, 0.8 }\n buttonParameters.color = { 0.9, 0.7, 0.5 }\n\n -- index 0: button for spawn mode\n buttonParameters.click_function = \"toggleSpawnMode\"\n buttonParameters.label = BUTTON_LABELS[\"spawn\"][spawnAll]\n buttonParameters.position = { x = 0.16, y = 0.1, z = 0.565 }\n buttonParameters.height = 375\n buttonParameters.width = 2300\n self.createButton(buttonParameters)\n\n -- index 1: button for search mode\n buttonParameters.click_function = \"toggleSearchMode\"\n buttonParameters.label = BUTTON_LABELS[\"search\"][searchExact]\n buttonParameters.position = { x = 0.16, y = 0.1, z = 0.652 }\n self.createButton(buttonParameters)\n\n -- index 2: start search\n buttonParameters.click_function = \"startSearch\"\n buttonParameters.label = \"\"\n buttonParameters.position = { x = 0, y = 0, z = 0.806 }\n buttonParameters.height = 600\n buttonParameters.width = 2800\n self.createButton(buttonParameters)\nend\n\nfunction toggleSpawnMode()\n spawnAll = not spawnAll\n self.editButton({ index = 0, label = BUTTON_LABELS[\"spawn\"][spawnAll] })\nend\n\nfunction toggleSearchMode()\n searchExact = not searchExact\n self.editButton({ index = 1, label = BUTTON_LABELS[\"search\"][searchExact] })\nend\n\n-- if \"Enter press\" (\\n) is found, start search and recreate input\nfunction input_func(_, _, input, stillEditing)\n if not stillEditing then\n inputParameters.value = input\n elseif string.find(input, \"%\\n\") ~= nil then\n inputParameters.value = input.gsub(input, \"%\\n\", \"\")\n startSearch()\n self.removeInput(0)\n self.createInput(inputParameters)\n end\nend\n\nfunction startSearch()\n if inputParameters.value == nil or string.len(inputParameters.value) == 0 then\n printToAll(\"Please enter a search string.\", \"Yellow\")\n return\n end\n\n if string.len(inputParameters.value) \u003c 3 then\n printToAll(\"Please enter a longer search string.\", \"Yellow\")\n return\n end\n\n if not allCardsBagApi.isBagPresent() then\n printToAll(\"Player card bag couldn't be found.\", \"Red\")\n return\n end\n\n -- search all objects in bag\n local cardList = allCardsBagApi.getCardsByName(inputParameters.value, searchExact)\n if cardList == nil or #cardList == 0 then\n printToAll(\"No match found.\", \"Red\")\n return\n end\n if (#cardList \u003e 100) then\n printToAll(\"Matched more than 100 cards, please try a more specific search.\", \"Yellow\")\n return\n end\n\n -- sort table by name (reverse for multiple results, because bottom card spawns first)\n table.sort(cardList, function(k1, k2) return spawnAll == (k1.data.Nickname \u003e k2.data.Nickname) end)\n\n local rot = self.getRotation()\n local pos = self.positionToWorld(Vector(0, 2, -0.08))\n Spawner.spawnCards(cardList, pos, rot, true)\nend\nend)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id table String ID of the card to retrieve\n ---@return table table\n -- If the indexes are still being constructed, an empty table is\n -- returned. Otherwise, a single table with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", {id = id})\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned\n -- it will be removed from the list and cannot be selected again until a reload\n -- occurs or the indexes are rebuilt, which will refresh the list to include all\n -- weaknesses.\n ---@return string: ID of the selected weakness.\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n return getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list. Note that this is\n -- an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return getAllCardsBag().call(\"getCardsByName\", {name = name, exact = exact})\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean true for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return getAllCardsBag().call(\"getCardsByClassAndLevel\", {class = class, upgraded = upgraded})\n end\n\n AllCardsBagApi.getCardsByCycle = function(cycle)\n return getAllCardsBag().call(\"getCardsByCycle\", cycle)\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return getAllCardsBag().call(\"getUniqueWeaknesses\")\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"playercards/PlayerCardSpawner\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Amount to shift for the next card (zShift) or next row of cards (xShift)\n-- Note that the table rotation is weird, and the X axis is vertical while the\n-- Z axis is horizontal\nlocal SPREAD_Z_SHIFT = -2.3\nlocal SPREAD_X_SHIFT = -3.66\n\nSpawner = { }\n\n-- Spawns a list of cards at the given position/rotation. This will separate cards by size -\n-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If\n-- there are different types, the provided callback will be called once for each type as it spawns\n-- either a card or deck.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos tts__Vector table where the cards should be spawned (global)\n---@param rot tts__Vector table for the orientation of the spawned cards (global)\n---@param sort boolean True if this list of cards should be sorted before spawning\n---@param callback? function Callback to be called after the card/deck spawns.\nSpawner.spawnCards = function(cardList, pos, rot, sort, callback)\n if (sort) then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local miniCards = { }\n local standardCards = { }\n local investigatorCards = { }\n\n for _, card in ipairs(cardList) do\n if (card.metadata.type == \"Investigator\") then\n table.insert(investigatorCards, card)\n elseif (card.metadata.type == \"Minicard\") then\n table.insert(miniCards, card)\n else\n table.insert(standardCards, card)\n end\n end\n -- Spawn each of the three types individually. Each Y position shift accounts for the thickness\n -- of the spawned deck\n local position = { x = pos.x, y = pos.y, z = pos.z }\n Spawner.spawn(investigatorCards, position, rot, callback)\n\n position.y = position.y + (#investigatorCards + #standardCards) * 0.07\n Spawner.spawn(standardCards, position, rot, callback)\n\n position.y = position.y + (#standardCards + #miniCards) * 0.07\n Spawner.spawn(miniCards, position, rot, callback)\nend\n\nSpawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)\n if (sort) then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local position = { x = startPos.x, y = startPos.y, z = startPos.z }\n -- Special handle the first row if we have less than a full single row, but only if there's a\n -- reasonable max column count. Single-row spreads will send a large value for maxCols\n if maxCols \u003c 100 and #cardList \u003c maxCols then\n position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)\n end\n local cardsInRow = 0\n local rows = 0\n for _, card in ipairs(cardList) do\n Spawner.spawn({ card }, position, rot, callback)\n position.z = position.z + SPREAD_Z_SHIFT\n cardsInRow = cardsInRow + 1\n if cardsInRow \u003e= maxCols then\n rows = rows + 1\n local cardsForRow = #cardList - rows * maxCols\n if cardsForRow \u003e maxCols then\n cardsForRow = maxCols\n end\n position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)\n position.x = position.x + SPREAD_X_SHIFT\n cardsInRow = 0\n end\n end\nend\n\n-- Spawn a specific list of cards. This method is for internal use and should not be called\n-- directly, use spawnCards instead.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos table Position where the cards should be spawned (global)\n---@param rot table Rotation for the orientation of the spawned cards (global)\n---@param callback? function callback to be called after the card/deck spawns.\nSpawner.spawn = function(cardList, pos, rot, callback)\n if #cardList == 0 then return end\n\n -- Spawn a single card directly\n if #cardList == 1 then\n -- handle sideways card\n if cardList[1].data.SidewaysCard then\n rot = { rot.x, rot.y - 90, rot.z }\n end\n spawnObjectData({\n data = cardList[1].data,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\n return\n end\n\n -- For multiple cards, construct a deck and spawn that\n local deck = Spawner.buildDeckDataTemplate()\n\n -- Decks won't inherently scale to the cards in them. The card list being spawned should be all\n -- the same type/size by this point, so use the first card to set the size\n deck.Transform = {\n scaleX = cardList[1].data.Transform.scaleX,\n scaleY = 1,\n scaleZ = cardList[1].data.Transform.scaleZ\n }\n\n local sidewaysDeck = true\n for _, spawnCard in ipairs(cardList) do\n Spawner.addCardToDeck(deck, spawnCard.data)\n -- set sidewaysDeck to false if any card is not a sideways card\n sidewaysDeck = (sidewaysDeck and spawnCard.data.SidewaysCard)\n end\n\n -- set the alt view angle for sideways decks\n if sidewaysDeck then\n deck.AltLookAngle = { x = 0, y = 180, z = 90 }\n rot = { rot.x, rot.y - 90, rot.z }\n end\n\n spawnObjectData({\n data = deck,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\nend\n\n-- Inserts a card into the given deck. This does three things:\n-- 1. Add the card's data to ContainedObjects\n-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's\n-- ID list. Note that the deck's ID list is \"DeckIDs\" even though it\n-- contains a list of card Ids\n-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's\n-- \"CustomDeck\" field is a list of all CustomDecks used by cards within the\n-- deck, keyed by the DeckID and referencing the custom deck table\n---@param deck table TTS deck data structure to add to\n---@param cardData table Data for the card to be inserted\nSpawner.addCardToDeck = function(deck, cardData)\n for customDeckId, customDeckData in pairs(cardData.CustomDeck) do\n if (deck.CustomDeck[customDeckId] == nil) then\n -- CustomDeck not added to deck yet, add it\n deck.CustomDeck[customDeckId] = customDeckData\n elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then\n -- CustomDeck for this card matches the current one for the deck, do nothing\n else\n -- CustomDeck data conflict\n local newDeckId = nil\n for deckId, customDeck in pairs(deck.CustomDeck) do\n if (customDeckData.FaceURL == customDeck.FaceURL) then\n newDeckId = deckId\n end\n end\n if (newDeckId == nil) then\n -- No non-conflicting custom deck for this card, add a new one\n newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, \"1000\")\n deck.CustomDeck[newDeckId] = customDeckData\n end\n -- Update the card with the new CustomDeck info\n cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)\n cardData.CustomDeck[customDeckId] = nil\n cardData.CustomDeck[newDeckId] = customDeckData\n break\n end\n end\n table.insert(deck.ContainedObjects, cardData)\n table.insert(deck.DeckIDs, cardData.CardID)\nend\n\n-- Create an empty deck data table which can have cards added to it. This\n-- creates a new table on each call without using metatables or previous\n-- definitions because we can't be sure that TTS doesn't modify the structure\n---@return table deck Table containing the minimal TTS deck data structure\nSpawner.buildDeckDataTemplate = function()\n local deck = {}\n deck.Name = \"Deck\"\n\n -- Card data. DeckIDs and CustomDeck entries will be built from the cards\n deck.ContainedObjects = {}\n deck.DeckIDs = {}\n deck.CustomDeck = {}\n\n -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here\n deck.Transform = {\n scaleX = 1,\n scaleY = 1,\n scaleZ = 1,\n }\n\n return deck\nend\n\n-- Returns the first ID which does not exist in the given table, starting at startId and increasing\n---@param objectTable table keyed by strings which are numbers\n---@param startId string possible ID.\n---@return string id \u003e= startId\nSpawner.findNextAvailableId = function(objectTable, startId)\n local id = startId\n while (objectTable[id] ~= nil) do\n id = tostring(tonumber(id) + 1)\n end\n return id\nend\n\n-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.\n---@return number PBCN 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are\n-- irrelevant as they provide only grouping and the order between them doesn't matter.\nSpawner.getpbcn = function(metadata)\n if metadata.permanent then\n return 1\n elseif metadata.bonded_to ~= nil then\n return 2\n else -- Normal card\n return 3\n end\nend\n\n-- Comparison function used to sort the cards in a deck. Groups bonded or\n-- permanent cards first, then sorts within theose types by name/subname.\n-- Normal cards will sort in standard alphabetical order, while\n-- permanent/bonded/customizable will be in reverse alphabetical order.\n--\n-- Since cards spawn in the order provided by this comparator, with the first\n-- cards ending up at the bottom of a pile, this ordering will spawn in reverse\n-- alphabetical order. This presents the cards in order for non-face-down\n-- areas, and presents them in order when Searching the face-down deck.\nSpawner.cardComparator = function(card1, card2)\n local pbcn1 = Spawner.getpbcn(card1.metadata)\n local pbcn2 = Spawner.getpbcn(card2.metadata)\n if pbcn1 ~= pbcn2 then\n return pbcn1 \u003e pbcn2\n end\n if pbcn1 == 3 then\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n end\n return card1.data.Description \u003c card2.data.Description\n else\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003e card2.data.Nickname\n end\n return card1.data.Description \u003e card2.data.Description\n end\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/CardSearch\")\nend)\n__bundle_register(\"playercards/CardSearch\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/PlayerCardSpawner\")\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n\nlocal BUTTON_LABELS = {}\nBUTTON_LABELS[\"spawn\"] = {}\nBUTTON_LABELS[\"spawn\"][true] = \"All matching cards\"\nBUTTON_LABELS[\"spawn\"][false] = \"First matching card\"\nBUTTON_LABELS[\"search\"] = {}\nBUTTON_LABELS[\"search\"][true] = \"Name equals search term\"\nBUTTON_LABELS[\"search\"][false] = \"Name contains search term\"\n\nlocal inputParameters = {}\ninputParameters.label = \"Click to enter card name\"\ninputParameters.input_function = \"input_func\"\ninputParameters.function_owner = self\ninputParameters.alignment = 2\ninputParameters.position = { x = 0, y = 0.1, z = -0.62 }\ninputParameters.width = 3750\ninputParameters.height = 380\ninputParameters.font_size = 350\ninputParameters.scale = { 0.1, 1, 0.1 }\ninputParameters.color = { 0.9, 0.7, 0.5 }\ninputParameters.font_color = { 0, 0, 0 }\n\nfunction onSave()\n return JSON.encode({ spawnAll, searchExact, inputParameters.value })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData)\n spawnAll = loadedData[1] or false\n searchExact = loadedData[2] or false\n inputParameters.value = loadedData[3] or \"\"\n self.createInput(inputParameters)\n\n -- shared parameters\n local buttonParameters = {}\n buttonParameters.function_owner = self\n buttonParameters.font_size = 180\n buttonParameters.scale = { 0.1, 1, 0.1 }\n buttonParameters.hover_color = { 0.4, 0.6, 0.8 }\n buttonParameters.color = { 0.9, 0.7, 0.5 }\n\n -- index 0: button for spawn mode\n buttonParameters.click_function = \"toggleSpawnMode\"\n buttonParameters.label = BUTTON_LABELS[\"spawn\"][spawnAll]\n buttonParameters.position = { x = 0.16, y = 0.1, z = 0.565 }\n buttonParameters.height = 375\n buttonParameters.width = 2300\n self.createButton(buttonParameters)\n\n -- index 1: button for search mode\n buttonParameters.click_function = \"toggleSearchMode\"\n buttonParameters.label = BUTTON_LABELS[\"search\"][searchExact]\n buttonParameters.position = { x = 0.16, y = 0.1, z = 0.652 }\n self.createButton(buttonParameters)\n\n -- index 2: start search\n buttonParameters.click_function = \"startSearch\"\n buttonParameters.label = \"\"\n buttonParameters.position = { x = 0, y = 0, z = 0.806 }\n buttonParameters.height = 600\n buttonParameters.width = 2800\n self.createButton(buttonParameters)\nend\n\nfunction toggleSpawnMode()\n spawnAll = not spawnAll\n self.editButton({ index = 0, label = BUTTON_LABELS[\"spawn\"][spawnAll] })\nend\n\nfunction toggleSearchMode()\n searchExact = not searchExact\n self.editButton({ index = 1, label = BUTTON_LABELS[\"search\"][searchExact] })\nend\n\n-- if \"Enter press\" (\\n) is found, start search and recreate input\nfunction input_func(_, _, input, stillEditing)\n if not stillEditing then\n inputParameters.value = input\n elseif string.find(input, \"%\\n\") ~= nil then\n inputParameters.value = input.gsub(input, \"%\\n\", \"\")\n startSearch()\n self.removeInput(0)\n self.createInput(inputParameters)\n end\nend\n\nfunction startSearch()\n if inputParameters.value == nil or string.len(inputParameters.value) == 0 then\n printToAll(\"Please enter a search string.\", \"Yellow\")\n return\n end\n\n if string.len(inputParameters.value) \u003c 3 then\n printToAll(\"Please enter a longer search string.\", \"Yellow\")\n return\n end\n\n if not allCardsBagApi.isBagPresent() then\n printToAll(\"Player card bag couldn't be found.\", \"Red\")\n return\n end\n\n -- search all objects in bag\n local cardList = allCardsBagApi.getCardsByName(inputParameters.value, searchExact)\n if cardList == nil or #cardList == 0 then\n printToAll(\"No match found.\", \"Red\")\n return\n end\n if (#cardList \u003e 100) then\n printToAll(\"Matched more than 100 cards, please try a more specific search.\", \"Yellow\")\n return\n end\n\n -- sort table by name (reverse for multiple results, because bottom card spawns first)\n table.sort(cardList, function(k1, k2) return spawnAll == (k1.data.Nickname \u003e k2.data.Nickname) end)\n\n local rot = self.getRotation()\n local pos = self.positionToWorld(Vector(0, 2, -0.08))\n Spawner.spawnCards(cardList, pos, rot, true)\nend\nend)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- internal function to create a copy of the table to avoid operating on variables owned by different objects\n local function returnCopyOfList(data)\n local copiedList = {}\n for _, id in ipairs(data) do\n table.insert(copiedList, id)\n end\n return copiedList\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id string ID of the card to retrieve\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a single table with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", { id = id })\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned it\n -- will be removed from the list and cannot be selected again until a reload occurs\n -- or the indexes are rebuilt, which will refresh the list to include all weaknesses.\n ---@return string: ID of the selected weakness\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list.\n -- Note that this is an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByName\", { name = name, exact = exact }))\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByClassAndLevel\", { class = class, upgraded = upgraded }))\n end\n\n -- Returns a list of cards from the bag matching a cycle\n ---@param cycle string Cycle to retrieve (\"The Scarlet Keys\" etc.)\n ---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByCycle\", { cycle = cycle, sortByMetadata = sortByMetadata }))\n end\n\n -- Constructs a list of available basic weaknesses by starting with the full pool of basic\n -- weaknesses then removing any which are currently in the play or deck construction areas\n ---@param traits? string Trait(s) to use as filter\n ---@return table: Array of weakness IDs which are valid to choose from\n AllCardsBagApi.buildAvailableWeaknesses = function(traits)\n return returnCopyOfList(getAllCardsBag().call(\"buildAvailableWeaknesses\", traits))\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return returnCopyOfList(getAllCardsBag().call(\"getUniqueWeaknesses\"))\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"playercards/PlayerCardSpawner\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Amount to shift for the next card (zShift) or next row of cards (xShift)\n-- Note that the table rotation is weird, and the X axis is vertical while the\n-- Z axis is horizontal\nlocal SPREAD_Z_SHIFT = -2.3\nlocal SPREAD_X_SHIFT = -3.66\n\nSpawner = { }\n\n-- Spawns a list of cards at the given position/rotation. This will separate cards by size -\n-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If\n-- there are different types, the provided callback will be called once for each type as it spawns\n-- either a card or deck.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos tts__Vector table where the cards should be spawned (global)\n---@param rot tts__Vector table for the orientation of the spawned cards (global)\n---@param sort boolean True if this list of cards should be sorted before spawning\n---@param callback? function Callback to be called after the card/deck spawns.\nSpawner.spawnCards = function(cardList, pos, rot, sort, callback)\n if sort then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local miniCards = { }\n local standardCards = { }\n local investigatorCards = { }\n\n for _, card in ipairs(cardList) do\n if card.metadata.type == \"Investigator\" then\n table.insert(investigatorCards, card)\n elseif card.metadata.type == \"Minicard\" then\n -- set proper scale for minicards\n card.data.Transform.scaleX = 0.6\n card.data.Transform.scaleZ = 0.6\n table.insert(miniCards, card)\n else\n table.insert(standardCards, card)\n end\n end\n\n -- Spawn each of the three types individually. Y position accounts for the thickness of the spawned deck\n local position = { x = pos.x, y = pos.y, z = pos.z }\n Spawner.spawn(investigatorCards, position, rot, callback)\n\n position.y = position.y + (#investigatorCards + #standardCards) * 0.07\n Spawner.spawn(standardCards, position, rot, callback)\n\n position.y = position.y + (#standardCards + #miniCards) * 0.07\n Spawner.spawn(miniCards, position, rot, callback)\nend\n\nSpawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)\n if sort then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local position = { x = startPos.x, y = startPos.y, z = startPos.z }\n -- Special handle the first row if we have less than a full single row, but only if there's a\n -- reasonable max column count. Single-row spreads will send a large value for maxCols\n if maxCols \u003c 100 and #cardList \u003c maxCols then\n position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)\n end\n local cardsInRow = 0\n local rows = 0\n for _, card in ipairs(cardList) do\n Spawner.spawn({ card }, position, rot, callback)\n position.z = position.z + SPREAD_Z_SHIFT\n cardsInRow = cardsInRow + 1\n if cardsInRow \u003e= maxCols then\n rows = rows + 1\n local cardsForRow = #cardList - rows * maxCols\n if cardsForRow \u003e maxCols then\n cardsForRow = maxCols\n end\n position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)\n position.x = position.x + SPREAD_X_SHIFT\n cardsInRow = 0\n end\n end\nend\n\n-- Spawn a specific list of cards. This method is for internal use and should not be called\n-- directly, use spawnCards instead.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos table Position where the cards should be spawned (global)\n---@param rot table Rotation for the orientation of the spawned cards (global)\n---@param callback? function callback to be called after the card/deck spawns.\nSpawner.spawn = function(cardList, pos, rot, callback)\n if #cardList == 0 then return end\n\n -- Spawn a single card directly\n if #cardList == 1 then\n -- handle sideways card\n if cardList[1].data.SidewaysCard then\n rot = { rot.x, rot.y - 90, rot.z }\n end\n return spawnObjectData({\n data = cardList[1].data,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- For multiple cards, construct a deck and spawn that\n local deckScaleX = cardList[1].data.Transform.scaleX\n local deckScaleZ = cardList[1].data.Transform.scaleZ\n local deck = Spawner.buildDeckDataTemplate(deckScaleX, deckScaleZ)\n\n local sidewaysDeck = true\n for _, spawnCard in ipairs(cardList) do\n Spawner.addCardToDeck(deck, spawnCard.data)\n -- set sidewaysDeck to false if any card is not a sideways card\n sidewaysDeck = (sidewaysDeck and spawnCard.data.SidewaysCard)\n end\n\n -- set the alt view angle for sideways decks\n if sidewaysDeck then\n deck.AltLookAngle = { x = 0, y = 180, z = 90 }\n rot = { rot.x, rot.y - 90, rot.z }\n end\n\n return spawnObjectData({\n data = deck,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\nend\n\n-- Inserts a card into the given deck. This does three things:\n-- 1. Add the card's data to ContainedObjects\n-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's\n-- ID list. Note that the deck's ID list is \"DeckIDs\" even though it\n-- contains a list of card Ids\n-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's\n-- \"CustomDeck\" field is a list of all CustomDecks used by cards within the\n-- deck, keyed by the DeckID and referencing the custom deck table\n---@param deck table TTS deck data structure to add to\n---@param cardData table Data for the card to be inserted\nSpawner.addCardToDeck = function(deck, cardData)\n for customDeckId, customDeckData in pairs(cardData.CustomDeck) do\n if (deck.CustomDeck[customDeckId] == nil) then\n -- CustomDeck not added to deck yet, add it\n deck.CustomDeck[customDeckId] = customDeckData\n elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then\n -- CustomDeck for this card matches the current one for the deck, do nothing\n else\n -- CustomDeck data conflict\n local newDeckId = nil\n for deckId, customDeck in pairs(deck.CustomDeck) do\n if (customDeckData.FaceURL == customDeck.FaceURL) then\n newDeckId = deckId\n end\n end\n if (newDeckId == nil) then\n -- No non-conflicting custom deck for this card, add a new one\n newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, \"1000\")\n deck.CustomDeck[newDeckId] = customDeckData\n end\n -- Update the card with the new CustomDeck info\n cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)\n cardData.CustomDeck[customDeckId] = nil\n cardData.CustomDeck[newDeckId] = customDeckData\n break\n end\n end\n table.insert(deck.ContainedObjects, cardData)\n table.insert(deck.DeckIDs, cardData.CardID)\nend\n\n-- Create an empty deck data table which can have cards added to it. This\n-- creates a new table on each call without using metatables or previous\n-- definitions because we can't be sure that TTS doesn't modify the structure\n---@return table deck Table containing the minimal TTS deck data structure\nSpawner.buildDeckDataTemplate = function(deckScaleX, deckScaleZ)\n local deck = {}\n deck.Name = \"Deck\"\n\n -- Card data. DeckIDs and CustomDeck entries will be built from the cards\n deck.ContainedObjects = {}\n deck.DeckIDs = {}\n deck.CustomDeck = {}\n\n -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here\n -- Decks won't inherently scale to the cards in them. The card list being spawned should be all\n -- the same type/size by this point, so use the first card to set the size\n deck.Transform = {\n scaleX = deckScaleX or 1,\n scaleY = 1,\n scaleZ = deckScaleZ or 1,\n }\n\n return deck\nend\n\n-- Returns the first ID which does not exist in the given table, starting at startId and increasing\n---@param objectTable table keyed by strings which are numbers\n---@param startId string possible ID.\n---@return string id \u003e= startId\nSpawner.findNextAvailableId = function(objectTable, startId)\n local id = startId\n while objectTable[id] ~= nil do\n id = tostring(tonumber(id) + 1)\n end\n return id\nend\n\n-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.\n---@return number PBCN 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are\n-- irrelevant as they provide only grouping and the order between them doesn't matter.\nSpawner.getpbcn = function(metadata)\n if metadata.permanent then\n return 1\n elseif metadata.bonded_to ~= nil then\n return 2\n else -- Normal card\n return 3\n end\nend\n\n-- Comparison function used to sort the cards in a deck. Groups bonded or\n-- permanent cards first, then sorts within theose types by name/subname.\n-- Normal cards will sort in standard alphabetical order, while\n-- permanent/bonded/customizable will be in reverse alphabetical order.\n--\n-- Since cards spawn in the order provided by this comparator, with the first\n-- cards ending up at the bottom of a pile, this ordering will spawn in reverse\n-- alphabetical order. This presents the cards in order for non-face-down\n-- areas, and presents them in order when Searching the face-down deck.\nSpawner.cardComparator = function(card1, card2)\n local pbcn1 = Spawner.getpbcn(card1.metadata)\n local pbcn2 = Spawner.getpbcn(card2.metadata)\n if pbcn1 ~= pbcn2 then\n return pbcn1 \u003e pbcn2\n end\n if pbcn1 == 3 then\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n end\n return card1.data.Description \u003c card2.data.Description\n else\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003e card2.data.Nickname\n end\n return card1.data.Description \u003e card2.data.Description\n end\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[true,false,\"\"]", "MeasureMovement": false, "Name": "Custom_Tile", @@ -195809,7 +139528,7 @@ "Transform": { "posX": 60, "posY": 1.481, - "posZ": 56, + "posZ": 57, "rotX": 0, "rotY": 270, "rotZ": 0, @@ -195855,7 +139574,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/PhaseTracker\")\nend)\n__bundle_register(\"accessories/PhaseTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal phaseNames = {\n \"I. Mythos Phase\",\n \"II. Investigation Phase\",\n \"III. Enemy Phase\",\n \"IV. Upkeep Phase\"\n}\nlocal phaseImages = {\n \"http://cloud-3.steamusercontent.com/ugc/933819604050849085/9E22AFD7B0157140FC177DBCCBCB1D61D6A0329F/\",\n \"http://cloud-3.steamusercontent.com/ugc/933819604050885611/845B5AA915F30492B5F34864698B9C3627FA5763/\",\n \"http://cloud-3.steamusercontent.com/ugc/982233321870235122/492996D07ABF6DDA4B605A3013C4892839DCF1F3/\",\n \"http://cloud-3.steamusercontent.com/ugc/982233321870237261/C287CAED2423970F33E72D6C7415CBEC6794C533/\"\n}\n\nlocal phaseId, broadcastChange\n\nfunction onSave()\n return JSON.encode({\n phaseId = phaseId,\n broadcastChange = broadcastChange\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n phaseId = loadedData.phaseId\n broadcastChange = loadedData.broadcastChange\n else\n phaseId = 1\n broadcastChange = false\n end\n\n self.createButton(\n {\n tooltip = \"change phase\",\n click_function = 'changeState',\n function_owner = self,\n width = 600,\n height = 600\n })\n\n self.addContextMenuItem(\"toggle broadcasting\", updateBroadcast)\nend\n\nfunction updateBroadcast()\n for _, tracker in ipairs(getObjectsWithTag(\"LinkedPhaseTracker\")) do\n tracker.setVar(\"broadcastChange\", not broadcastChange)\n end\n broadcastToAll(\"Broadcasting phase changes has been \" .. (broadcastChange and \"enabled.\" or \"disabled.\"))\nend\n\nfunction changeState(_, _, isRightClick)\n -- get newId for all trackers\n local newId = phaseId + (isRightClick and -1 or 1)\n if newId == 0 then\n newId = 4\n elseif newId == 5 then\n newId = 1\n end\n\n -- broadcast if option is enabled\n if broadcastChange then\n broadcastToAll(phaseNames[newId])\n end\n\n -- manipulate data and then respawn\n local data = self.getData()\n data[\"CustomImage\"][\"ImageURL\"] = phaseImages[newId]\n data[\"CustomImage\"][\"ImageSecondaryURL\"] = phaseImages[newId]\n data[\"LuaScriptState\"] = \"{\\\"broadcastChange\\\":\" .. tostring(broadcastChange) .. \",\\\"phaseId\\\":\" .. newId .. \"}\"\n\n -- update all trackers with tag\n for _, tracker in ipairs(getObjectsWithTag(\"LinkedPhaseTracker\")) do\n local pos = tracker.getPosition()\n local rot = tracker.getRotation()\n local scale = tracker.getScale()\n tracker.destruct()\n spawnObjectData(\n {\n data = data,\n position = pos,\n rotation = rot,\n scale = scale\n }\n )\n end\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/PhaseTracker\")\nend)\n__bundle_register(\"accessories/PhaseTracker\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal phaseNames = {\n \"I. Mythos Phase\",\n \"II. Investigation Phase\",\n \"III. Enemy Phase\",\n \"IV. Upkeep Phase\"\n}\nlocal phaseImages = {\n \"http://cloud-3.steamusercontent.com/ugc/933819604050849085/9E22AFD7B0157140FC177DBCCBCB1D61D6A0329F/\",\n \"http://cloud-3.steamusercontent.com/ugc/933819604050885611/845B5AA915F30492B5F34864698B9C3627FA5763/\",\n \"http://cloud-3.steamusercontent.com/ugc/982233321870235122/492996D07ABF6DDA4B605A3013C4892839DCF1F3/\",\n \"http://cloud-3.steamusercontent.com/ugc/982233321870237261/C287CAED2423970F33E72D6C7415CBEC6794C533/\"\n}\n\nfunction onSave()\n return JSON.encode({\n phaseId = phaseId,\n broadcastChange = broadcastChange\n })\nend\n\nfunction loadFromSaveTable(savedData)\n for var, val in pairs(JSON.decode(savedData)) do\n _G[var] = val\n end\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n loadFromSaveTable(savedData)\n else\n phaseId = 1\n broadcastChange = false\n end\n\n self.createButton(\n {\n tooltip = \"change phase\",\n click_function = 'changeState',\n function_owner = self,\n width = 600,\n height = 600,\n color = { r = 0, g = 0, b = 0, a = 0 }\n })\n\n self.addContextMenuItem(\"Toggle Broadcasting\", updateBroadcast)\nend\n\nfunction updateBroadcast(playerColor)\n Player[playerColor].clearSelectedObjects()\n for _, tracker in ipairs(getObjectsWithTag(\"LinkedPhaseTracker\")) do\n tracker.setVar(\"broadcastChange\", not broadcastChange)\n end\n broadcastToAll(\"Broadcasting phase changes has been \" .. (broadcastChange and \"enabled.\" or \"disabled.\"))\nend\n\nfunction changeState(_, _, isRightClick)\n -- get newId for all trackers\n local newId = phaseId + (isRightClick and -1 or 1)\n if newId == 0 then\n newId = 4\n elseif newId == 5 then\n newId = 1\n end\n\n -- broadcast if option is enabled\n if broadcastChange then\n broadcastToAll(phaseNames[newId])\n end\n\n -- manipulate data and then respawn\n local data = self.getData()\n data[\"CustomImage\"][\"ImageURL\"] = phaseImages[newId]\n data[\"CustomImage\"][\"ImageSecondaryURL\"] = phaseImages[newId]\n data[\"LuaScriptState\"] = \"{\\\"broadcastChange\\\":\" .. tostring(broadcastChange) .. \",\\\"phaseId\\\":\" .. newId .. \"}\"\n\n -- update all trackers with tag\n for _, tracker in ipairs(getObjectsWithTag(\"LinkedPhaseTracker\")) do\n local pos = tracker.getPosition()\n local rot = tracker.getRotation()\n local scale = tracker.getScale()\n tracker.destruct()\n spawnObjectData(\n {\n data = data,\n position = pos,\n rotation = rot,\n scale = scale\n }\n )\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"broadcastChange\":false,\"phaseId\":1}", "MeasureMovement": false, "Name": "Custom_Tile", @@ -200297,7 +144016,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GenericCounter\")\nend)\n__bundle_register(\"core/GenericCounter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nMIN_VALUE = 0\nMAX_VALUE = 99\nval = 0\n\nfunction onSave() return JSON.encode(val) end\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n val = JSON.decode(savedData)\n end\n\n local name = self.getName()\n local position = { 0, 0.06, 0 }\n\n -- set position of label depending on object\n if name == \"Damage\" or name == \"Resources\" or name == \"Resource Counter\" then\n position = { 0, 0.06, 0.1 }\n elseif name == \"Horror\" then\n position = { -0.025, 0.06, -0.025 }\n elseif name == \"Elder Sign Counter\" or name == \"Auto-fail Counter\" then\n position = { 0, 0.1, 0 }\n end\n\n self.createButton({\n label = tostring(val),\n click_function = \"addOrSubtract\",\n function_owner = self,\n position = position,\n height = 600,\n width = 1000,\n scale = { 1.5, 1.5, 1.5 },\n font_size = 600,\n font_color = { 1, 1, 1, 100 },\n color = { 0, 0, 0, 0 }\n })\n\n -- add context menu entries\n self.addContextMenuItem(\"Add 5\", function() updateVal(val + 5) end)\n self.addContextMenuItem(\"Subtract 5\", function() updateVal(val - 5) end)\n self.addContextMenuItem(\"Add 10\", function() updateVal(val + 10) end)\n self.addContextMenuItem(\"Subtract 10\", function() updateVal(val - 10) end)\nend\n\nfunction updateVal(newVal)\n if tonumber(newVal) then\n val = math.min(math.max(newVal, MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\n end\nend\n\nfunction addOrSubtract(_, _, isRightClick)\n val = math.min(math.max(val + (isRightClick and -1 or 1), MIN_VALUE), MAX_VALUE)\n self.editButton({ index = 0, label = tostring(val) })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "0", "MeasureMovement": false, "Memo": "resourceCounter", @@ -200328,9 +144047,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 0.24706, - "g": 0.24706, - "r": 0.24706 + "b": 1, + "g": 1, + "r": 1 }, "CustomImage": { "CustomTile": { @@ -200340,14 +144059,21 @@ "Type": 2 }, "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", "WidthScale": 0 }, - "Description": "Action Token", + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", "DragSelectable": true, "GMNotes": "", - "GUID": "04765c", + "GUID": "834ad5", "Grid": true, "GridProjection": false, "Hands": false, @@ -200355,2246 +144081,22 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", - "LuaScriptState": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", "MeasureMovement": false, - "Memo": "actionToken", + "Memo": "universalActionAbility", "Name": "Custom_Tile", - "Nickname": "Neutral", + "Nickname": "Universal Action / Ability Token", "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0bcce1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -53.1985855, - "posY": 1.55004013, - "posZ": -22.5279942, - "rotX": 359.983826, - "rotY": 269.989624, - "rotZ": 359.9841, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "10": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120675/493ACE1FA05ED4DC96CC7F6D85B3488378C15DD2/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120736/F53989F0806C796D180647A16C6BB4E9957F6DBF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "68f249", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue Engage/Fight Action", - "Snap": true, - "States": { - "1": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.329411745, - "r": 0.07450979 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115513/9CA3B804F167041F03C9E0687378FF7B5DCDE1B8/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524115014/6CBF573A12494524613C6280F558D4BED97CF007/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e4b2b6", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242507, - "posY": 0.959991634, - "posZ": -3.66628647, - "rotX": 3.19422554e-7, - "rotY": 269.987976, - "rotZ": -0.00009157829, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "6": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.247059911, - "g": 0.247059911, - "r": 0.247059911 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355435567056295/5A6DE2C637AADCD147723211020D8C0D0591EAE7/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120440/08045D95997033A4D64764850FC2B68C4FB12A3C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2691e1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425337, - "posY": 0.959991634, - "posZ": -3.66629124, - "rotX": -0.0000184208129, - "rotY": 269.9878, - "rotZ": -0.000116128736, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425489, - "posY": 0.959991634, - "posZ": -3.66629434, - "rotX": -0.0000277218132, - "rotY": 269.987732, - "rotZ": -0.000125872859, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "11": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119623/2244A30B5EBB4126F0BE1D2FF61F6C824DFEE58D/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119692/042FBF813801CFDF4FEDA9ED3205D331842975FA/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "befce9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Evade Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970487, - "posY": 0.959991634, - "posZ": -5.93830156, - "rotX": -0.0000076235865, - "rotY": 269.9841, - "rotZ": -0.000118024727, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "12": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.2117644, - "g": 0.2823527, - "r": 0.06666646 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120530/79626D1941BEE2D2A310FD4B7C8E3CE90E6820AB/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120607/0D0F1B80B4E6A20B4728F1F7582FF09C1D4A3B9F/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b439e3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Parley Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -8.970486, - "posY": 0.959991634, - "posZ": -7.074313, - "rotX": 0.0000133775629, - "rotY": 269.988464, - "rotZ": -0.0000981453049, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "13": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391777, - "g": 0.07058794, - "r": 0.321568251 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121068/E62DCFA57CE5FE6AF021A2F07C6650323BE19C93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121138/A5506FADCE917AA99925516A147E0320322B5BDD/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72f18", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Spell Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.834444, - "posY": 0.9599933, - "posZ": -3.666303, - "rotX": -0.0000930833849, - "rotY": 269.979767, - "rotZ": -0.0000140167895, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "14": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470339, - "g": 0.117646806, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119758/EE686A3287D3399347AD72140474F599585E68D5/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119839/A92D5AF13B283117BD62EE84B657A3A71FBBD274/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bbd286", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Play Item Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 0.9599919, - "posZ": -4.80229, - "rotX": -0.0000181347659, - "rotY": 269.9885, - "rotZ": -0.000124786486, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "15": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516201848/72B3B9E2B59F25FEC82412AC22245D03655A4558/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11508f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Guardian", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.83447456, - "posY": 1.019994, - "posZ": -5.93830156, - "rotX": 0.00007209413, - "rotY": 269.988525, - "rotZ": 0.0000200837931, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "16": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516265983/F50A6212D30C442429ED22B8CC8FD24D4CB76A2A/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8888ff", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -7.834475, - "posY": 1.01999056, - "posZ": -7.074312, - "rotX": -0.00008478706, - "rotY": 270.009827, - "rotZ": -0.00007123187, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "17": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515898740/E92441671B056D4CDF99DF9E6C88BE6598AAB50F/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7f001b", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698466, - "posY": 1.01999128, - "posZ": -3.66628671, - "rotX": 0.00007766587, - "rotY": 270.000031, - "rotZ": 0.000128919462, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "18": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722516557267/757887224F6C37104CDFFE241FAD09B57117D670/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6bd479", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.019991, - "posZ": -4.80229, - "rotX": 0.0000896203055, - "rotY": 269.999969, - "rotZ": 0.000128821237, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "19": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/952965722515960460/F43F63452854B10B416FDF3BF9EF3068E6E68F26/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "172d0e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.69846535, - "posY": 1.01999176, - "posZ": -5.938301, - "rotX": 0.000100863988, - "rotY": 270.000061, - "rotZ": 0.0000739920142, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120939/5A99D70BBAA96A7CCE94CBAA01BC8C9352F59174/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121002/727C40B7A122B3EC91AD1EF76741A9888E1FF0FF/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "45b80c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Seeker", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425117, - "posY": 0.959991634, - "posZ": -3.66628766, - "rotX": -0.00000112005034, - "rotY": 269.987976, - "rotZ": -0.00009159232, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "20": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/950722873599303195/BAB8BB40C755C099128931212969243EFF56ED39/", - "MaterialIndex": 3, - "MeshURL": "https://pastebin.com/raw/ALrYhQGb", - "NormalURL": "", - "TypeIndex": 4 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2d0664", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Model", - "Nickname": "Neutral", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -6.698465, - "posY": 1.01999319, - "posZ": -7.07431269, - "rotX": 0.00007148875, - "rotY": 270.000122, - "rotZ": 0.00009776296, - "scaleX": 0.45, - "scaleY": 0.6, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.211764365, - "g": 0.282352656, - "r": 0.06666643 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120799/1AA70B46183E3DC9981CD93D0A289D456C368B15/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120870/FFA52250CDBE4067D16226E7B4C8D2E6BF263C5B/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6cd9a4", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Rogue", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425175, - "posY": 0.959991634, - "posZ": -3.66628838, - "rotX": -0.0000136311965, - "rotY": 269.987885, - "rotZ": -0.00010959358, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "4": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.380391747, - "g": 0.07058791, - "r": 0.321568221 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120230/08DDB68E10023CC76B9450989F3526F9744A9F77/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120299/D6B1AAFF9763CD6F410D56A716D731714DE34EF8/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "484748", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Mystic", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425222, - "posY": 0.959991634, - "posZ": -3.66628933, - "rotX": -0.0000148262206, - "rotY": 269.987854, - "rotZ": -0.000109348286, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "5": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.176470309, - "g": 0.117646776, - "r": 0.745098054 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121217/CBDB66CA029638728CE27CCBD335BDCFF25B6BCE/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121301/8A83B84C4EC594D48259904616769E84C5191F83/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "59124e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Survivor", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.242528, - "posY": 0.959991634, - "posZ": -3.66629028, - "rotX": -0.0000209040336, - "rotY": 269.9878, - "rotZ": -0.000119170043, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "7": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.647058845, - "g": 0.3294117, - "r": 0.07450976 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119903/366BC6E113AE8B9BE480617CEC6BE564CF37CE93/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524119975/196A6AB09BE31462712BA7DF6F6698762B3FC98D/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "29d645", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Guardian Engage/Fight Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425385, - "posY": 0.959991634, - "posZ": -3.66629219, - "rotX": -0.0000287290841, - "rotY": 269.9878, - "rotZ": -0.000127649575, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "8": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195674, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120070/74F06CA8602C110158A32ADFF9E1FC1FB858612B/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524120154/9E0936305F807390EBA6AB130E498BFEDBA7596C/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "85047f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Investigate Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425432, - "posY": 0.959991634, - "posZ": -3.66629314, - "rotX": -0.0000293503817, - "rotY": 269.987823, - "rotZ": -0.000127514912, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - }, - "9": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.290195644, - "g": 0.5803921, - "r": 0.894117653 - }, - "CustomImage": { - "CustomTile": { - "Stackable": false, - "Stretch": true, - "Thickness": 0.1, - "Type": 2 - }, - "ImageScalar": 1, - "ImageSecondaryURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121381/43FEB2F56E57A5B72E6E7F02E138539D5BB42AC1/", - "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2037355191524121455/F21B46B06BBF327601B4F8A5F9F00974149A6752/", - "WidthScale": 0 - }, - "Description": "Action Token", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2c6c38", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_Tile", - "Nickname": "Tome Action", - "Snap": true, - "Sticky": true, - "Tags": [ - "ActionToken" - ], - "Tooltip": true, - "Transform": { - "posX": -11.2425547, - "posY": 0.959991634, - "posZ": -3.66629553, - "rotX": -0.0000261616078, - "rotY": 269.987671, - "rotZ": -0.00012405579, - "scaleX": 0.45, - "scaleY": 1, - "scaleZ": 0.45 - }, - "Value": 0, - "XmlUI": "" - } - }, "Sticky": true, "Tags": [ - "ActionToken" + "UniversalToken" ], "Tooltip": true, "Transform": { - "posX": -53.2, - "posY": 1.55, - "posZ": 9.67, + "posX": -50.981, + "posY": 1.75, + "posZ": 7.323, "rotX": 0, "rotY": 270, "rotZ": 0, @@ -202649,7 +144151,7 @@ }, "Autoraise": true, "ColorDiffuse": { - "a": 0.25, + "a": 0.75, "b": 0.25, "g": 0.25, "r": 0.25 @@ -202726,7 +144228,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/tour/TourManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n require(\"core/tour/TourScript\")\n require(\"core/tour/TourCard\")\n local TourManager = {}\n local internal = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- Base IDs for various tour card UI elements. Actual IDs will have _[playerColor] appended\n local CARD_ID = \"tourCard\"\n local LEFT_NARRATOR_ID = \"tourNarratorImageLeft\"\n local RIGHT_NARRATOR_ID = \"tourNarratorImageRight\"\n local BUBBLE_ID = \"tourSpeechBubble\"\n local TEXT_ID = \"tourText\"\n local NEXT_BUTTON_ID = \"tourNext\"\n local STOP_BUTTON_ID = \"tourStop\"\n\n -- Table centerpoint for the camera hook object. Camera handling is a bit erratic so it doesn't\n -- always land right where you think it's going to, but it's close\n local HOOK_CAMERA_HOME = {\n x = -30.2,\n y = 60,\n z = 0,\n }\n\n -- Default (0) position for the camera, as defined in the mod. If we don't recreate this position\n -- EXACTLY when exiting the tour then camera controls get weird\n local DEFAULT_CAMERA_POS = {\n position = { x = -22.26, y = -2.5, z = 5.26 },\n pitch = 64.34,\n yaw = 90,\n distance = 104\n }\n\n -- Global XML coordinates where we can present a card\n local SCREEN_POSITIONS = {\n center = \"0 0 0\",\n north = \"0 300 0\",\n east = \"600 0 0\",\n west = \"-600 0 0\",\n south = \"0 -300 0\",\n\n -- Northwest is only used by the Mandy card, move it a little right than standard so it's\n -- closer to the importer\n northwest = \"-500 300 0\",\n northeast = \"600 300 0\",\n southwest = \"-600 -300 0\",\n\n -- Used by the Diana and Wini cards referencing the bottom-right global controls, moved a little\n -- closer to them\n southeast = \"730 -365 0\"\n }\n\n -- Tracks the current state of the tours. Keyed by player color to keep each player's tour\n -- separate, will hold the camera hook and current card.\n local tourState = {}\n\n -- Kicks off the tour by initializing the card and camera hook. A callback on the hook creation\n -- will then show the first card.\n ---@param playerColor string Player color to start the tour for\n TourManager.startTour = function(playerColor)\n tourState[playerColor] = {\n currentCardIndex = 1\n }\n -- Camera gets really screwy when we finalize if we don't start settled in ThirdPerson at the\n -- default position before attaching to the hook. Unfortunately there are no callbacks for when\n -- the movement is done, but the delay seems to handle it\n Player[playerColor].setCameraMode(\"ThirdPerson\")\n Player[playerColor].lookAt(DEFAULT_CAMERA_POS)\n\n -- Initial camera rotation is painfully slow. White and Orange players are likely oriented\n -- correctly, but need a longer start delay for Green and Red\n local delay = 0.5\n if playerColor ~= \"White\" and playerColor ~= \"Orange\" then\n delay = 2\n broadcastToColor(\"Starting the tour, please wait...\", playerColor)\n end\n Wait.time(function()\n internal.createTourCard(playerColor)\n -- XML update to add the new card takes a few frames to load, wait for it to finish then create the hook\n Wait.condition(function() internal.createCameraHook(playerColor) end, function() return not Global.UI.loading end)\n end, delay)\n end\n\n -- Shows the next card in the tour script. This method is exposed (rather than being part of\n -- internal) because the XMLUI callbacks expect the method to be on the object directly.\n ---@param player tts__Player object to show the next card for, provided by XMLUI callback\n function nextCard(player)\n internal.hideCard(player.color)\n Wait.time(function()\n tourState[player.color].currentCardIndex = tourState[player.color].currentCardIndex + 1\n if tourState[player.color].currentCardIndex \u003e #TOUR_SCRIPT then\n internal.finalizeTour(player.color)\n else\n internal.showCurrentCard(player.color)\n end\n end, 0.3)\n end\n\n -- Ends the tour and cleans up the camera. This method is exposed (rather than being part of\n -- internal) because the XMLUI callbacks expect the method to be on the object directly.\n ---@param player tts__Player object to end the tour for, provided by XMLUI callback\n function stopTour(player)\n internal.hideCard(player.color)\n Wait.time(function()\n internal.finalizeTour(player.color)\n end, 0.3)\n end\n\n -- Updates the card UI for the script at the current index, moves the camera to the proper\n -- position, and shows the card.\n ---@param playerColor string Player color to show the current card for\n internal.showCurrentCard = function(playerColor)\n internal.updateCardDisplay(playerColor)\n local delay = 0\n local cardIndex = tourState[playerColor].currentCardIndex\n local hook = getObjectFromGUID(tourState[playerColor].cameraHookGuid)\n\n if not TOUR_SCRIPT[cardIndex].skipCentering then\n hook.setPositionSmooth(HOOK_CAMERA_HOME, false, false)\n delay = delay + 0.5\n end\n local lookPos\n local objReferenceData = TOUR_SCRIPT[cardIndex].objReferenceData\n if objReferenceData ~= nil then\n local lookAtObj = guidReferenceApi.getObjectByOwnerAndType(objReferenceData.owner, objReferenceData.type)\n lookPos = lookAtObj.getPosition()\n lookPos.y = TOUR_SCRIPT[cardIndex].distanceFromObj or 0\n -- Since camera isn't directly above the hook, changing the Y affects the visual position of\n -- whatever object we're trying to look at. This is an approximation, but close enough to\n -- keep the object more centered\n lookPos.x = lookPos.x - lookPos.y / 2\n elseif TOUR_SCRIPT[cardIndex].showPos ~= nil then\n lookPos = TOUR_SCRIPT[cardIndex].showPos\n end\n if lookPos ~= nil then\n Wait.time(function()\n hook.setPositionSmooth(lookPos, false, false)\n end, delay)\n delay = delay + 0.5\n end\n Wait.time(function() Global.UI.show(internal.getUiId(CARD_ID, playerColor)) end, delay)\n end\n\n -- Hides the current card being shown to a player. This can be in preparation for showing the\n -- next card, or ending the tour.\n ---@param playerColor string Player color to hide the current card for\n internal.hideCard = function(playerColor)\n Global.UI.hide(internal.getUiId(CARD_ID, playerColor))\n end\n\n -- Cleans up all the various resources associated with the tour, and (hopefully) resets the\n -- camera to the default position. Camera handling is erratic, the final card in the script\n -- should include instructions for the player to fix it.\n ---@param playerColor string Player color to clean up\n internal.finalizeTour = function(playerColor)\n local cameraHook = getObjectFromGUID(tourState[playerColor].cameraHookGuid)\n cameraHook.destruct()\n Player[playerColor].setCameraMode(\"ThirdPerson\")\n tourState[playerColor] = nil\n Wait.frames(function()\n Player[playerColor].lookAt(DEFAULT_CAMERA_POS)\n end, 3)\n end\n\n -- Updates the card UI to show the appropriate card configuration.\n ---@param playerColor string Player color to update card for\n internal.updateCardDisplay = function(playerColor)\n local index = tourState[playerColor].currentCardIndex\n Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), \"image\", \"Inv-\" .. TOUR_SCRIPT[index].narrator)\n Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), \"image\", \"Inv-\" .. TOUR_SCRIPT[index].narrator)\n Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), \"text\", \"\\\"\" .. TOUR_SCRIPT[index].text .. \"\\\"\")\n local cardPos = TOUR_SCRIPT[index].position or \"north\"\n Global.UI.setAttribute(internal.getUiId(CARD_ID, playerColor), \"position\", SCREEN_POSITIONS[cardPos])\n Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), \"active\", index \u003c #TOUR_SCRIPT)\n\n -- Adjust images so the narrator is on the left or right, as defined by the card\n if TOUR_SCRIPT[index].speakerSide == \"right\" then\n Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), \"active\", false)\n Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), \"active\", true)\n Global.UI.setAttribute(internal.getUiId(BUBBLE_ID, playerColor), \"rotation\", \"0 180 0\")\n Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), \"offsetXY\", \"-15 -15\")\n Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), \"offsetXY\", \"-35 -45\")\n Global.UI.setAttribute(internal.getUiId(STOP_BUTTON_ID, playerColor), \"offsetXY\", \"5 -45\")\n else\n Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), \"active\", true)\n Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), \"active\", false)\n Global.UI.setAttribute(internal.getUiId(BUBBLE_ID, playerColor), \"rotation\", \"0 0 0\")\n Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), \"offsetXY\", \"15 -15\")\n Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), \"offsetXY\", \"-5 -45\")\n Global.UI.setAttribute(internal.getUiId(STOP_BUTTON_ID, playerColor), \"offsetXY\", \"35 -45\")\n end\n end\n\n -- Creates a small, transparent object which the camera will be attached to in order to move the\n -- user's view around the table. This should be called only at the beginning of the tour. Once\n -- creation is complete the user's camera will be attached to the hook and the first card will be\n -- shown.\n ---@param playerColor string Player color to create the hook for\n internal.createCameraHook = function(playerColor)\n local hookData = {\n Name = \"BlockSquare\",\n Transform = {\n posX = HOOK_CAMERA_HOME.x,\n posY = HOOK_CAMERA_HOME.y,\n posZ = HOOK_CAMERA_HOME.z,\n rotX = 0,\n rotY = 270,\n rotZ = 0,\n scaleX = 0.1,\n scaleY = 0.1,\n scaleZ = 0.1,\n },\n ColorDiffuse = {\n r = 0,\n g = 0,\n b = 0,\n a = 0,\n },\n Locked = true,\n GMNotes = playerColor\n }\n\n spawnObjectData({ data = hookData, callback_function = internal.onHookCreated })\n end\n\n -- Callback for creation of the camera hook object. Will attach the camera and show the current\n -- (presumably first) card.\n ---@param hook tts__Object Created object\n internal.onHookCreated = function(hook)\n local playerColor = hook.getGMNotes()\n tourState[playerColor].cameraHookGuid = hook.getGUID()\n Player[playerColor].attachCameraToObject({\n object = hook,\n offset = { x = -20, y = 30, z = 0 }\n })\n internal.showCurrentCard(playerColor)\n end\n\n -- Creates an XMLUI entry in Global for a player-specific tour card. Dynamically creating this\n -- is somewhat complex, but ensures we can properly handle any player color.\n ---@param playerColor string Player color to create the card for\n internal.createTourCard = function(playerColor)\n -- Make sure the card doesn't exist before we create a new one\n if Global.UI.getAttributes(internal.getUiId(CARD_ID, playerColor)) ~= nil then return end\n \n tourCardTemplate.attributes.id = internal.getUiId(CARD_ID, playerColor)\n tourCardTemplate.children[1].attributes.id = internal.getUiId(LEFT_NARRATOR_ID, playerColor)\n tourCardTemplate.children[2].attributes.id = internal.getUiId(RIGHT_NARRATOR_ID, playerColor)\n tourCardTemplate.children[3].attributes.id = internal.getUiId(BUBBLE_ID, playerColor)\n tourCardTemplate.children[4].attributes.id = internal.getUiId(TEXT_ID, playerColor)\n tourCardTemplate.children[5].attributes.id = internal.getUiId(NEXT_BUTTON_ID, playerColor)\n tourCardTemplate.children[5].attributes.onClick = self.getGUID() .. \"/nextCard\"\n tourCardTemplate.children[6].attributes.id = internal.getUiId(STOP_BUTTON_ID, playerColor)\n tourCardTemplate.children[6].attributes.onClick = self.getGUID() .. \"/stopTour\"\n internal.setDeepVisibility(tourCardTemplate, playerColor)\n\n local globalXml = Global.UI.getXmlTable()\n table.insert(globalXml, tourCardTemplate)\n Global.UI.setXmlTable(globalXml)\n end\n\n -- Panels don't cause their children to inherit their visibility value, so this recurses down the\n -- XML table to set all children to the same visibility.\n ---@param xmlUi table Lua table describing the XML\n ---@param playerColor string String color of the player to make this visible for\n internal.setDeepVisibility = function(xmlUi, playerColor)\n xmlUi.attributes.visibility = \"\" .. playerColor\n if xmlUi.children ~= nil then\n for _, child in ipairs(xmlUi.children) do\n internal.setDeepVisibility(child, playerColor)\n end\n end\n end\n\n internal.getUiId = function(baseId, playerColor)\n return baseId .. \"_\" .. playerColor\n end\n\n return TourManager\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/tour/TourCard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Table definition for the tour card layout. This is functionally XMLUI in Lua form, but using\n-- this for dynamic creation ensures we can handle any player color without needing 10\n-- near-duplicate definitions in Global.xml\n\ntourCardTemplate = {\n tag = \"Panel\",\n attributes = {\n id = \"tourCard\",\n height = 215,\n width = 330,\n rotation = \"0 0 0\",\n position = \"0 300 30\",\n showAnimation = \"FadeIn\",\n hideAnimation = \"FadeOut\",\n active = false\n },\n children = {\n {\n tag = \"Image\",\n attributes = {\n id = \"tourNarratorImageLeft\",\n height = 120,\n width = 80,\n rectAlignment = \"UpperLeft\",\n offsetXY = \"-80 0\"\n -- Image will be set when the card is updated\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourNarratorImageRight\",\n active = false,\n height = 125,\n width = 80,\n rectAlignment = \"UpperRight\",\n offsetXY = \"80 0\"\n -- Image will be set when the card is updated\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourSpeechBubble\",\n color = \"#F5F5DC\",\n height = 215,\n width = 330,\n rectAlignment = \"MiddleCenter\",\n image = \"SpeechBubble\"\n }\n },\n {\n tag = \"Text\",\n attributes = {\n id = \"tourText\",\n -- Everything on this is double-sized and scaled down to keep the text sharps\n height = 370,\n width = 520,\n scale = \"0.5 0.5 1\",\n rectAlignment = \"UpperCenter\",\n offsetXY = \"15 -15\",\n resizeTextForBestFit = true,\n resizeTextMinSize = 20,\n resizeTextMaxSize = 32,\n color = \"#050505\",\n alignment = \"UpperLeft\",\n horizontalOverflow = \"wrap\"\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourNext\",\n height = 45,\n width = 45,\n rectAlignment = \"LowerRight\",\n offsetXY = \"-5 -45\",\n image = \"NextArrow\"\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourStop\",\n height = 45,\n width = 45,\n rectAlignment = \"LowerLeft\",\n offsetXY = \"35 -45\",\n image = \"Exit\"\n }\n }\n }\n}\nend)\n__bundle_register(\"core/tour/TourScript\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Script for the SCED tour. Documentation and definitions to come.\n\nTOUR_SCRIPT = {\n {\n narrator = \"Roland\",\n text = \"Despite my best efforts, looks like you found us. You may live to regret that. As long as you're here though we might as well show you around.\\n\\nUse the arrow to move forward, and if the horrors get to be too much you can quit whenever you like. Ready to get started?\",\n position = \"center\"\n },\n {\n narrator = \"Darrell\",\n text = \"Cameras can be tricky things. Best you leave handling it to the professionals during the tour. Don't try to move the camera until the tour is complete.\\n\\nOnce we're done, remember you can use the 'p' key to switch back to third-person mode, and the spacebar to reset the position.\",\n position = \"center\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Daisy\",\n text = \"If you're new to the game, the library here has everything you'll need. A little research can go a long way, and looking into old newspapers for the weird and unusual can yield some surprisingly helpful information.\\n\\nI put a few right there that might prove enlightening.\",\n objReferenceData = { owner = \"Mythos\", type = \"RulesReference\" },\n distanceFromObj = 20,\n position = \"west\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Mandy\",\n text = \"To survive what's coming you'll need a deck. If it's safely hidden away on ArkhamDB you can load it here, and even find the newest version after an upgrade without changing the ID.\\n\\nNo need to publish all your decks, use 'Private' and you can see it. Just make sure to select 'Make your decks public' in ArkhamDB.\",\n objReferenceData = { owner = \"Mythos\", type = \"DeckImporter\" },\n distanceFromObj = -5,\n position = \"northwest\",\n skipCentering = true\n },\n {\n narrator = \"Daniela\",\n text = \"I prefer the hands-on approach to building things, if you do too you can build a deck yourself.\\n\\nAll the cards you could ever need are here, laid out like a disassembled engine. Place the cards on the table, copy them for your deck, and you'll be ready for anything.\",\n objReferenceData = { owner = \"Mythos\", type = \"PlayerCardPanel\" },\n distanceFromObj = -7,\n position = \"south\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Finn\",\n text = \"Ready to face the unknown? We've smuggled shocking revelations and devious enemies from all over the world. Download the campaign you want to play, then Place it on the table to see the scenarios.\\n\\nJust remember - if it turns out to be too much for you, I was never here.\",\n objReferenceData = { owner = \"Mythos\", type = \"CampaignThePathToCarcosa\" },\n distanceFromObj = 20,\n position = \"northwest\",\n skipCentering = true\n },\n {\n narrator = \"Diana\",\n text = \"These symbols on the bottom right are a repository of arcane knowledge, containing all the official content to download plus some deviously creative works from fans. One should beware those who seem too fond of the darkness, but you cannot deny the quality of their efforts.\\n\\nDon't see anything here? Only promoted players can access these.\",\n position = \"southeast\"\n },\n {\n narrator = \"Winifred\",\n text = \"No good aviator would fly a plane she didn't know and hadn't tweaked a bit herself. The gear icon contains settings to customize your play experience, from alternate ways to track your clues to a variety of helpers to streamline the game.\\n\\nEverything here is optional, but who doesn't want to go as fast as they can? Just remember that all settings affect all players, so strap in and trust your pilot!\",\n position = \"southeast\"\n },\n {\n narrator = \"Amina\",\n text = \"This is the Mythos area. Encounter cards, acts, and agenda will all be placed here while the large map below is where you will be exploring - be sure to set the number of investigators!\\n\\nYou can count doom on the agenda by clicking the large counter, and the smaller will automatically count doom tokens on the table. The chaos bag is in that book over on the right, and you can add or remove tokens from it whenever you need.\",\n showPos = { x = -2.85, y = 0, z = 0.55 },\n position = \"north\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Gloria\",\n text = \"The evils that lurk in this world are out there, creeping ever closer. When they find you, this will easily draw a card from the encounter deck. The deck will even reshuffle itself when needed, for the enemies we face are unending.\",\n showPos = { x = -35, y = -20, z = 28 },\n position = \"west\",\n skipCentering = true\n },\n {\n narrator = \"Jacqueline\",\n text = \"When the ire of fate finds you and the chaos looms, this large button will draw a chaos token. Click it again to return the token to the bag.\\n\\nWhether a vision of the future or a curse from the opponents we face, if you need additional tokens a right-click will draw more. I wish you luck, but have a vision of red tentacles reaching for you...\",\n showPos = { x = -35, y = -20, z = 4.25 },\n position = \"north\",\n skipCentering = true,\n speakerSide = \"right\"\n },\n {\n narrator = \"Kohaku\",\n text = \"Folklorists, immersed in the rich narratives of blessings and curses, explore the essence of human beliefs. You can use this tool to control the amount of bless and curse tokens in the chaos bag. It will also display the amount in the bag (+ sealed on cards) for you. Remember to remove bless / curse tokens with this after resolving them.\",\n objReferenceData = { owner = \"Mythos\", type = \"BlessCurseManager\" },\n position = \"center\",\n skipCentering = true,\n speakerSide = \"left\"\n },\n {\n narrator = \"Preston\",\n text = \"I can afford to buy what I need, but for those less well-off we've provided an endless pool of tokens to track your game. Simply drag one out of the pools here.\\n\\nResources are my favorite of course, but damage and horror are as inevitable as taxes. I leave those to my bookkeeper though. Those tokens can work like counters, use the number keys to change the value.\",\n objReferenceData = { owner = \"Mythos\", type = \"ResourceTokenBag\" },\n position = \"north\",\n skipCentering = true,\n speakerSide = \"right\"\n },\n {\n narrator = \"Norman\",\n text = \"That's the end of the tour, but there's much more to discover if you look in the right places. Some cards have helpers on the right-click menu, and every new version adds new content and functions.\\n\\nDon't be afraid to explore, and best of luck out there! We'll all need it...\",\n position = \"center\",\n speakerSide = \"right\"\n }\n}\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/tour/TourStarter\")\nend)\n__bundle_register(\"core/tour/TourStarter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal tourManager = require(\"core/tour/TourManager\")\n\nfunction onLoad()\n self.createButton({\n click_function = \"startTour\",\n function_owner = self,\n position = { 1.27, 0.05, 0.035},\n width = 500,\n height = 20,\n color = { 0, 0, 0, 0 },\n -- TTS has a minium height for buttons, have to scale the Z-axis down to get the right size\n scale = { 1, 1, 0.82 },\n tooltip = \"Start the Tour\",\n })\n self.createButton({\n click_function = \"deleteStarter\",\n function_owner = self,\n position = { 1.27, 0.05, 0.309},\n width = 500,\n height = 20,\n color = { 0, 0, 0, 0 },\n -- TTS has a minium height for buttons, have to scale the Z-axis down to get the right size\n scale = { 1, 1, 0.82 },\n tooltip = \"Delete this Panel\",\n })\nend\n\nfunction startTour(_, playerColor, _)\n tourManager.startTour(playerColor)\nend\n\nfunction deleteStarter(_, _, _)\n self.destruct()\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/tour/TourStarter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal tourManager = require(\"core/tour/TourManager\")\n\nfunction onLoad()\n self.createButton({\n click_function = \"startTour\",\n function_owner = self,\n position = { 1.27, 0.05, 0.035},\n width = 500,\n height = 20,\n color = { 0, 0, 0, 0 },\n -- TTS has a minium height for buttons, have to scale the Z-axis down to get the right size\n scale = { 1, 1, 0.82 },\n tooltip = \"Start the Tour\",\n })\n self.createButton({\n click_function = \"deleteStarter\",\n function_owner = self,\n position = { 1.27, 0.05, 0.309},\n width = 500,\n height = 20,\n color = { 0, 0, 0, 0 },\n -- TTS has a minium height for buttons, have to scale the Z-axis down to get the right size\n scale = { 1, 1, 0.82 },\n tooltip = \"Delete this Panel\",\n })\nend\n\nfunction startTour(_, playerColor, _)\n tourManager.startTour(playerColor)\nend\n\nfunction deleteStarter(_, _, _)\n self.destruct()\nend\nend)\n__bundle_register(\"core/tour/TourManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n require(\"core/tour/TourScript\")\n require(\"core/tour/TourCard\")\n local TourManager = {}\n local internal = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- Base IDs for various tour card UI elements. Actual IDs will have _[playerColor] appended\n local CARD_ID = \"tourCard\"\n local LEFT_NARRATOR_ID = \"tourNarratorImageLeft\"\n local RIGHT_NARRATOR_ID = \"tourNarratorImageRight\"\n local BUBBLE_ID = \"tourSpeechBubble\"\n local TEXT_ID = \"tourText\"\n local NEXT_BUTTON_ID = \"tourNext\"\n local STOP_BUTTON_ID = \"tourStop\"\n\n -- Table centerpoint for the camera hook object. Camera handling is a bit erratic so it doesn't\n -- always land right where you think it's going to, but it's close\n local HOOK_CAMERA_HOME = {\n x = -30.2,\n y = 60,\n z = 0,\n }\n\n -- Default (0) position for the camera, as defined in the mod. If we don't recreate this position\n -- EXACTLY when exiting the tour then camera controls get weird\n local DEFAULT_CAMERA_POS = {\n position = { x = -22.26, y = -2.5, z = 5.26 },\n pitch = 64.34,\n yaw = 90,\n distance = 104\n }\n\n -- Global XML coordinates where we can present a card\n local SCREEN_POSITIONS = {\n center = \"0 0 0\",\n north = \"0 300 0\",\n east = \"600 0 0\",\n west = \"-600 0 0\",\n south = \"0 -300 0\",\n\n -- Northwest is only used by the Mandy card, move it a little right than standard so it's\n -- closer to the importer\n northwest = \"-500 300 0\",\n northeast = \"600 300 0\",\n southwest = \"-600 -300 0\",\n\n -- Used by the Diana and Wini cards referencing the bottom-right global controls, moved a little\n -- closer to them\n southeast = \"730 -365 0\"\n }\n\n -- Tracks the current state of the tours. Keyed by player color to keep each player's tour\n -- separate, will hold the camera hook and current card.\n local tourState = {}\n\n -- Kicks off the tour by initializing the card and camera hook. A callback on the hook creation\n -- will then show the first card.\n ---@param playerColor string Player color to start the tour for\n TourManager.startTour = function(playerColor)\n tourState[playerColor] = {\n currentCardIndex = 1\n }\n -- Camera gets really screwy when we finalize if we don't start settled in ThirdPerson at the\n -- default position before attaching to the hook. Unfortunately there are no callbacks for when\n -- the movement is done, but the delay seems to handle it\n Player[playerColor].setCameraMode(\"ThirdPerson\")\n Player[playerColor].lookAt(DEFAULT_CAMERA_POS)\n\n -- Initial camera rotation is painfully slow. White and Orange players are likely oriented\n -- correctly, but need a longer start delay for Green and Red\n local delay = 0.5\n if playerColor ~= \"White\" and playerColor ~= \"Orange\" then\n delay = 2\n broadcastToColor(\"Starting the tour, please wait...\", playerColor)\n end\n Wait.time(function()\n internal.createTourCard(playerColor)\n -- XML update to add the new card takes a few frames to load, wait for it to finish then create the hook\n Wait.condition(function() internal.createCameraHook(playerColor) end, function() return not Global.UI.loading end)\n end, delay)\n end\n\n -- Shows the next card in the tour script. This method is exposed (rather than being part of\n -- internal) because the XMLUI callbacks expect the method to be on the object directly.\n ---@param player tts__Player object to show the next card for, provided by XMLUI callback\n function nextCard(player)\n internal.hideCard(player.color)\n Wait.time(function()\n tourState[player.color].currentCardIndex = tourState[player.color].currentCardIndex + 1\n if tourState[player.color].currentCardIndex \u003e #TOUR_SCRIPT then\n internal.finalizeTour(player.color)\n else\n internal.showCurrentCard(player.color)\n end\n end, 0.3)\n end\n\n -- Ends the tour and cleans up the camera. This method is exposed (rather than being part of\n -- internal) because the XMLUI callbacks expect the method to be on the object directly.\n ---@param player tts__Player object to end the tour for, provided by XMLUI callback\n function stopTour(player)\n internal.hideCard(player.color)\n Wait.time(function()\n internal.finalizeTour(player.color)\n end, 0.3)\n end\n\n -- Updates the card UI for the script at the current index, moves the camera to the proper\n -- position, and shows the card.\n ---@param playerColor string Player color to show the current card for\n internal.showCurrentCard = function(playerColor)\n internal.updateCardDisplay(playerColor)\n local delay = 0\n local cardIndex = tourState[playerColor].currentCardIndex\n local hook = getObjectFromGUID(tourState[playerColor].cameraHookGuid)\n\n if not TOUR_SCRIPT[cardIndex].skipCentering then\n hook.setPositionSmooth(HOOK_CAMERA_HOME, false, false)\n delay = delay + 0.5\n end\n local lookPos\n local objReferenceData = TOUR_SCRIPT[cardIndex].objReferenceData\n if objReferenceData ~= nil then\n local lookAtObj = guidReferenceApi.getObjectByOwnerAndType(objReferenceData.owner, objReferenceData.type)\n lookPos = lookAtObj.getPosition()\n lookPos.y = TOUR_SCRIPT[cardIndex].distanceFromObj or 0\n -- Since camera isn't directly above the hook, changing the Y affects the visual position of\n -- whatever object we're trying to look at. This is an approximation, but close enough to\n -- keep the object more centered\n lookPos.x = lookPos.x - lookPos.y / 2\n elseif TOUR_SCRIPT[cardIndex].showPos ~= nil then\n lookPos = TOUR_SCRIPT[cardIndex].showPos\n end\n if lookPos ~= nil then\n Wait.time(function()\n hook.setPositionSmooth(lookPos, false, false)\n end, delay)\n delay = delay + 0.5\n end\n Wait.time(function() Global.UI.show(internal.getUiId(CARD_ID, playerColor)) end, delay)\n end\n\n -- Hides the current card being shown to a player. This can be in preparation for showing the\n -- next card, or ending the tour.\n ---@param playerColor string Player color to hide the current card for\n internal.hideCard = function(playerColor)\n Global.UI.hide(internal.getUiId(CARD_ID, playerColor))\n end\n\n -- Cleans up all the various resources associated with the tour, and (hopefully) resets the\n -- camera to the default position. Camera handling is erratic, the final card in the script\n -- should include instructions for the player to fix it.\n ---@param playerColor string Player color to clean up\n internal.finalizeTour = function(playerColor)\n local cameraHook = getObjectFromGUID(tourState[playerColor].cameraHookGuid)\n cameraHook.destruct()\n Player[playerColor].setCameraMode(\"ThirdPerson\")\n tourState[playerColor] = nil\n Wait.frames(function()\n Player[playerColor].lookAt(DEFAULT_CAMERA_POS)\n end, 3)\n end\n\n -- Updates the card UI to show the appropriate card configuration.\n ---@param playerColor string Player color to update card for\n internal.updateCardDisplay = function(playerColor)\n local index = tourState[playerColor].currentCardIndex\n Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), \"image\", \"Inv-\" .. TOUR_SCRIPT[index].narrator)\n Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), \"image\", \"Inv-\" .. TOUR_SCRIPT[index].narrator)\n Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), \"text\", \"\\\"\" .. TOUR_SCRIPT[index].text .. \"\\\"\")\n local cardPos = TOUR_SCRIPT[index].position or \"north\"\n Global.UI.setAttribute(internal.getUiId(CARD_ID, playerColor), \"position\", SCREEN_POSITIONS[cardPos])\n Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), \"active\", index \u003c #TOUR_SCRIPT)\n\n -- Adjust images so the narrator is on the left or right, as defined by the card\n if TOUR_SCRIPT[index].speakerSide == \"right\" then\n Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), \"active\", false)\n Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), \"active\", true)\n Global.UI.setAttribute(internal.getUiId(BUBBLE_ID, playerColor), \"rotation\", \"0 180 0\")\n Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), \"offsetXY\", \"-15 -15\")\n Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), \"offsetXY\", \"-35 -45\")\n Global.UI.setAttribute(internal.getUiId(STOP_BUTTON_ID, playerColor), \"offsetXY\", \"5 -45\")\n else\n Global.UI.setAttribute(internal.getUiId(LEFT_NARRATOR_ID, playerColor), \"active\", true)\n Global.UI.setAttribute(internal.getUiId(RIGHT_NARRATOR_ID, playerColor), \"active\", false)\n Global.UI.setAttribute(internal.getUiId(BUBBLE_ID, playerColor), \"rotation\", \"0 0 0\")\n Global.UI.setAttribute(internal.getUiId(TEXT_ID, playerColor), \"offsetXY\", \"15 -15\")\n Global.UI.setAttribute(internal.getUiId(NEXT_BUTTON_ID, playerColor), \"offsetXY\", \"-5 -45\")\n Global.UI.setAttribute(internal.getUiId(STOP_BUTTON_ID, playerColor), \"offsetXY\", \"35 -45\")\n end\n end\n\n -- Creates a small, transparent object which the camera will be attached to in order to move the\n -- user's view around the table. This should be called only at the beginning of the tour. Once\n -- creation is complete the user's camera will be attached to the hook and the first card will be\n -- shown.\n ---@param playerColor string Player color to create the hook for\n internal.createCameraHook = function(playerColor)\n local hookData = {\n Name = \"BlockSquare\",\n Transform = {\n posX = HOOK_CAMERA_HOME.x,\n posY = HOOK_CAMERA_HOME.y,\n posZ = HOOK_CAMERA_HOME.z,\n rotX = 0,\n rotY = 270,\n rotZ = 0,\n scaleX = 0.1,\n scaleY = 0.1,\n scaleZ = 0.1,\n },\n ColorDiffuse = {\n r = 0,\n g = 0,\n b = 0,\n a = 0,\n },\n Locked = true,\n GMNotes = playerColor\n }\n\n spawnObjectData({ data = hookData, callback_function = internal.onHookCreated })\n end\n\n -- Callback for creation of the camera hook object. Will attach the camera and show the current\n -- (presumably first) card.\n ---@param hook tts__Object Created object\n internal.onHookCreated = function(hook)\n local playerColor = hook.getGMNotes()\n tourState[playerColor].cameraHookGuid = hook.getGUID()\n Player[playerColor].attachCameraToObject({\n object = hook,\n offset = { x = -20, y = 30, z = 0 }\n })\n internal.showCurrentCard(playerColor)\n end\n\n -- Creates an XMLUI entry in Global for a player-specific tour card. Dynamically creating this\n -- is somewhat complex, but ensures we can properly handle any player color.\n ---@param playerColor string Player color to create the card for\n internal.createTourCard = function(playerColor)\n -- Make sure the card doesn't exist before we create a new one\n if Global.UI.getAttributes(internal.getUiId(CARD_ID, playerColor)) ~= nil then return end\n \n tourCardTemplate.attributes.id = internal.getUiId(CARD_ID, playerColor)\n tourCardTemplate.children[1].attributes.id = internal.getUiId(LEFT_NARRATOR_ID, playerColor)\n tourCardTemplate.children[2].attributes.id = internal.getUiId(RIGHT_NARRATOR_ID, playerColor)\n tourCardTemplate.children[3].attributes.id = internal.getUiId(BUBBLE_ID, playerColor)\n tourCardTemplate.children[4].attributes.id = internal.getUiId(TEXT_ID, playerColor)\n tourCardTemplate.children[5].attributes.id = internal.getUiId(NEXT_BUTTON_ID, playerColor)\n tourCardTemplate.children[5].attributes.onClick = self.getGUID() .. \"/nextCard\"\n tourCardTemplate.children[6].attributes.id = internal.getUiId(STOP_BUTTON_ID, playerColor)\n tourCardTemplate.children[6].attributes.onClick = self.getGUID() .. \"/stopTour\"\n internal.setDeepVisibility(tourCardTemplate, playerColor)\n\n local globalXml = Global.UI.getXmlTable()\n table.insert(globalXml, tourCardTemplate)\n Global.UI.setXmlTable(globalXml)\n end\n\n -- Panels don't cause their children to inherit their visibility value, so this recurses down the\n -- XML table to set all children to the same visibility.\n ---@param xmlUi table Lua table describing the XML\n ---@param playerColor string String color of the player to make this visible for\n internal.setDeepVisibility = function(xmlUi, playerColor)\n xmlUi.attributes.visibility = \"\" .. playerColor\n if xmlUi.children ~= nil then\n for _, child in ipairs(xmlUi.children) do\n internal.setDeepVisibility(child, playerColor)\n end\n end\n end\n\n internal.getUiId = function(baseId, playerColor)\n return baseId .. \"_\" .. playerColor\n end\n\n return TourManager\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/tour/TourCard\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Table definition for the tour card layout. This is functionally XMLUI in Lua form, but using\n-- this for dynamic creation ensures we can handle any player color without needing 10\n-- near-duplicate definitions in Global.xml\n\ntourCardTemplate = {\n tag = \"Panel\",\n attributes = {\n id = \"tourCard\",\n height = 215,\n width = 330,\n rotation = \"0 0 0\",\n position = \"0 300 30\",\n showAnimation = \"FadeIn\",\n hideAnimation = \"FadeOut\",\n active = false\n },\n children = {\n {\n tag = \"Image\",\n attributes = {\n id = \"tourNarratorImageLeft\",\n height = 120,\n width = 80,\n rectAlignment = \"UpperLeft\",\n offsetXY = \"-80 0\"\n -- Image will be set when the card is updated\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourNarratorImageRight\",\n active = false,\n height = 125,\n width = 80,\n rectAlignment = \"UpperRight\",\n offsetXY = \"80 0\"\n -- Image will be set when the card is updated\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourSpeechBubble\",\n color = \"#F5F5DC\",\n height = 215,\n width = 330,\n rectAlignment = \"MiddleCenter\",\n image = \"SpeechBubble\"\n }\n },\n {\n tag = \"Text\",\n attributes = {\n id = \"tourText\",\n -- Everything on this is double-sized and scaled down to keep the text sharps\n height = 370,\n width = 520,\n scale = \"0.5 0.5 1\",\n rectAlignment = \"UpperCenter\",\n offsetXY = \"15 -15\",\n resizeTextForBestFit = true,\n resizeTextMinSize = 20,\n resizeTextMaxSize = 32,\n color = \"#050505\",\n alignment = \"UpperLeft\",\n horizontalOverflow = \"wrap\"\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourNext\",\n height = 45,\n width = 45,\n rectAlignment = \"LowerRight\",\n offsetXY = \"-5 -45\",\n image = \"NextArrow\"\n }\n },\n {\n tag = \"Image\",\n attributes = {\n id = \"tourStop\",\n height = 45,\n width = 45,\n rectAlignment = \"LowerLeft\",\n offsetXY = \"35 -45\",\n image = \"Exit\"\n }\n }\n }\n}\nend)\n__bundle_register(\"core/tour/TourScript\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Script for the SCED tour. Documentation and definitions to come.\n\nTOUR_SCRIPT = {\n {\n narrator = \"Roland\",\n text = \"Despite my best efforts, looks like you found us. You may live to regret that. As long as you're here though we might as well show you around.\\n\\nUse the arrow to move forward, and if the horrors get to be too much you can quit whenever you like. Ready to get started?\",\n position = \"center\"\n },\n {\n narrator = \"Darrell\",\n text = \"Cameras can be tricky things. Best you leave handling it to the professionals during the tour. Don't try to move the camera until the tour is complete.\\n\\nOnce we're done, remember you can use the 'p' key to switch back to third-person mode, and the spacebar to reset the position.\",\n position = \"center\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Daisy\",\n text = \"If you're new to the game, the library here has everything you'll need. A little research can go a long way, and looking into old newspapers for the weird and unusual can yield some surprisingly helpful information.\\n\\nI put a few right there that might prove enlightening.\",\n objReferenceData = { owner = \"Mythos\", type = \"RulesReference\" },\n distanceFromObj = 20,\n position = \"west\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Mandy\",\n text = \"To survive what's coming you'll need a deck. If it's safely hidden away on ArkhamDB you can load it here, and even find the newest version after an upgrade without changing the ID.\\n\\nNo need to publish all your decks, use 'Private' and you can see it. Just make sure to select 'Make your decks public' in ArkhamDB.\",\n objReferenceData = { owner = \"Mythos\", type = \"DeckImporter\" },\n distanceFromObj = -5,\n position = \"northwest\",\n skipCentering = true\n },\n {\n narrator = \"Daniela\",\n text = \"I prefer the hands-on approach to building things, if you do too you can build a deck yourself.\\n\\nAll the cards you could ever need are here, laid out like a disassembled engine. Place the cards on the table, copy them for your deck, and you'll be ready for anything.\",\n objReferenceData = { owner = \"Mythos\", type = \"PlayerCardPanel\" },\n distanceFromObj = -7,\n position = \"south\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Finn\",\n text = \"Ready to face the unknown? We've smuggled shocking revelations and devious enemies from all over the world. Download the campaign you want to play, then Place it on the table to see the scenarios.\\n\\nJust remember - if it turns out to be too much for you, I was never here.\",\n objReferenceData = { owner = \"Mythos\", type = \"CampaignThePathToCarcosa\" },\n distanceFromObj = 20,\n position = \"northwest\",\n skipCentering = true\n },\n {\n narrator = \"Diana\",\n text = \"These symbols on the bottom right are a repository of arcane knowledge, containing all the official content to download plus some deviously creative works from fans. One should beware those who seem too fond of the darkness, but you cannot deny the quality of their efforts.\\n\\nDon't see anything here? Only promoted players can access these.\",\n position = \"southeast\"\n },\n {\n narrator = \"Winifred\",\n text = \"No good aviator would fly a plane she didn't know and hadn't tweaked a bit herself. The gear icon contains settings to customize your play experience, from alternate ways to track your clues to a variety of helpers to streamline the game.\\n\\nEverything here is optional, but who doesn't want to go as fast as they can? Just remember that all settings affect all players, so strap in and trust your pilot!\",\n position = \"southeast\"\n },\n {\n narrator = \"Amina\",\n text = \"This is the Mythos area. Encounter cards, acts, and agenda will all be placed here while the large map below is where you will be exploring - be sure to set the number of investigators!\\n\\nYou can count doom on the agenda by clicking the large counter, and the smaller will automatically count doom tokens on the table. The chaos bag is in that book over on the right, and you can add or remove tokens from it whenever you need.\",\n showPos = { x = -2.85, y = 0, z = 0.55 },\n position = \"north\",\n speakerSide = \"right\"\n },\n {\n narrator = \"Gloria\",\n text = \"The evils that lurk in this world are out there, creeping ever closer. When they find you, this will easily draw a card from the encounter deck. The deck will even reshuffle itself when needed, for the enemies we face are unending.\",\n showPos = { x = -35, y = -20, z = 28 },\n position = \"west\",\n skipCentering = true\n },\n {\n narrator = \"Jacqueline\",\n text = \"When the ire of fate finds you and the chaos looms, this large button will draw a chaos token. Click it again to return the token to the bag.\\n\\nWhether a vision of the future or a curse from the opponents we face, if you need additional tokens a right-click will draw more. I wish you luck, but have a vision of red tentacles reaching for you...\",\n showPos = { x = -35, y = -20, z = 4.25 },\n position = \"north\",\n skipCentering = true,\n speakerSide = \"right\"\n },\n {\n narrator = \"Kohaku\",\n text = \"Folklorists, immersed in the rich narratives of blessings and curses, explore the essence of human beliefs. You can use this tool to control the amount of bless and curse tokens in the chaos bag. It will also display the amount in the bag (+ sealed on cards) for you. Remember to remove bless / curse tokens with this after resolving them.\",\n objReferenceData = { owner = \"Mythos\", type = \"BlessCurseManager\" },\n position = \"center\",\n skipCentering = true,\n speakerSide = \"left\"\n },\n {\n narrator = \"Preston\",\n text = \"I can afford to buy what I need, but for those less well-off we've provided an endless pool of tokens to track your game. Simply drag one out of the pools here.\\n\\nResources are my favorite of course, but damage and horror are as inevitable as taxes. I leave those to my bookkeeper though. Those tokens can work like counters, use the number keys to change the value.\",\n objReferenceData = { owner = \"Mythos\", type = \"ResourceTokenBag\" },\n position = \"north\",\n skipCentering = true,\n speakerSide = \"right\"\n },\n {\n narrator = \"Norman\",\n text = \"That's the end of the tour, but there's much more to discover if you look in the right places. Some cards have helpers on the right-click menu, and every new version adds new content and functions.\\n\\nDon't be afraid to explore, and best of luck out there! We'll all need it...\",\n position = \"center\",\n speakerSide = \"right\"\n }\n}\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/tour/TourStarter\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Token", @@ -202735,9 +144237,9 @@ "Sticky": true, "Tooltip": false, "Transform": { - "posX": -23.676, + "posX": -24.5, "posY": 1.57, - "posZ": 0.024, + "posZ": 0, "rotX": 0, "rotY": 270, "rotZ": 0, @@ -202792,7 +144294,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"arkhamdb/InstructionGenerator\")\nend)\n__bundle_register(\"arkhamdb/InstructionGenerator\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal idList = {}\n\nfunction onLoad()\n -- \"generate\" button\n local buttonParameters = {}\n buttonParameters.function_owner = self\n buttonParameters.height = 200\n buttonParameters.width = 1200\n buttonParameters.font_size = 75\n buttonParameters.click_function = \"generate\"\n buttonParameters.label = \"Generate instructions!\"\n buttonParameters.position = { 0, 0.06, 1.55 }\n self.createButton(buttonParameters)\n\n -- \"output\" text field\n local inputParameters = {}\n inputParameters.label = \"Click button above\"\n inputParameters.input_function = \"none\"\n inputParameters.function_owner = self\n inputParameters.position = { 0, 0.05, 1.95 }\n inputParameters.width = 1200\n inputParameters.height = 130\n inputParameters.font_size = 107\n self.createInput(inputParameters)\nend\n\n-- generates a string for the ArkhamDB deck notes that will instruct the deck import to add the specified cards\nfunction generate(_, playerColor)\n idList = {}\n for _, obj in ipairs(searchLib.onObject(self, \"isCardOrDeck\")) do\n if obj.type == \"Card\" then\n processCard(obj.getGMNotes(), obj.getName(), playerColor)\n elseif obj.type == \"Deck\" then\n for _, deepObj in ipairs(obj.getData().ContainedObjects) do\n processCard(deepObj.GMNotes, deepObj.Nickname, playerColor)\n end\n end\n end\n\n if #idList == 0 then\n broadcastToColor(\"Didn't find any valid cards.\", playerColor, \"Red\")\n return\n else\n broadcastToColor(\"Created deck instruction for \" .. #idList .. \" card(s). Copy it from the input field.\", playerColor, \"Green\")\n end\n\n -- sort the idList\n table.sort(idList, sortById)\n\n -- construct the string (new line for each instruction)\n local description = \"++SCED import instructions++\"\n for _, entry in ipairs(idList) do\n description = description .. \"\\n- add: \" .. entry.id .. \" (**\" .. entry.name .. \"**)\"\n end\n\n self.editInput({index = 0, value = description})\nend\n\n-- use the ZoopGuid as fallback if no id present\nfunction getIdFromData(metadata)\n if metadata.id then\n return metadata.id\n elseif metadata.TtsZoopGuid then\n return metadata.TtsZoopGuid\n end\nend\n\nfunction processCard(notes, name, playerColor)\n local id = getIdFromData(JSON.decode(notes) or {})\n if id then\n table.insert(idList, {id = id, name = name})\n else\n broadcastToColor(\"Couldn't get ID for \" .. name .. \".\", playerColor, \"Red\")\n end\nend\n\nfunction sortById(a, b)\n local numA = tonumber(a.id)\n local numB = tonumber(b.id)\n\n if numA and numB then\n return numA \u003c numB\n else\n return a.name \u003c b.name\n end\nend\n\nfunction none() end\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"arkhamdb/InstructionGenerator\")\nend)\n__bundle_register(\"arkhamdb/InstructionGenerator\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\n\nlocal idList = {}\n\nfunction onLoad()\n -- \"generate\" button\n local buttonParameters = {}\n buttonParameters.function_owner = self\n buttonParameters.height = 200\n buttonParameters.width = 1200\n buttonParameters.font_size = 75\n buttonParameters.click_function = \"generate\"\n buttonParameters.label = \"Generate instructions!\"\n buttonParameters.position = { 0, 0.06, 1.55 }\n self.createButton(buttonParameters)\n\n -- \"output\" text field\n local inputParameters = {}\n inputParameters.label = \"Click button above\"\n inputParameters.input_function = \"none\"\n inputParameters.function_owner = self\n inputParameters.position = { 0, 0.05, 1.95 }\n inputParameters.width = 1200\n inputParameters.height = 130\n inputParameters.font_size = 107\n self.createInput(inputParameters)\nend\n\n-- generates a string for the ArkhamDB deck notes that will instruct the deck import to add the specified cards\nfunction generate(_, playerColor)\n idList = {}\n for _, obj in ipairs(searchLib.onObject(self, \"isCardOrDeck\")) do\n if obj.type == \"Card\" then\n processCard(obj.getGMNotes(), obj.getName(), playerColor)\n elseif obj.type == \"Deck\" then\n for _, deepObj in ipairs(obj.getData().ContainedObjects) do\n processCard(deepObj.GMNotes, deepObj.Nickname, playerColor)\n end\n end\n end\n\n if #idList == 0 then\n broadcastToColor(\"Didn't find any valid cards.\", playerColor, \"Red\")\n return\n else\n broadcastToColor(\"Created deck instruction for \" .. #idList .. \" card(s). Copy it from the input field.\", playerColor, \"Green\")\n end\n\n -- sort the idList\n table.sort(idList, sortById)\n\n -- construct the string (new line for each instruction)\n local description = \"++SCED import instructions++\"\n for _, entry in ipairs(idList) do\n description = description .. \"\\n- add: \" .. entry.id .. \" (**\" .. entry.name .. \"**)\"\n end\n\n self.editInput({index = 0, value = description})\nend\n\n-- use the ZoopGuid as fallback if no id present\nfunction getIdFromData(metadata)\n if metadata.id then\n return metadata.id\n elseif metadata.TtsZoopGuid then\n return metadata.TtsZoopGuid\n end\nend\n\nfunction processCard(notes, name, playerColor)\n local id = getIdFromData(JSON.decode(notes) or {})\n if id then\n table.insert(idList, {id = id, name = name})\n else\n broadcastToColor(\"Couldn't get ID for \" .. name .. \".\", playerColor, \"Red\")\n end\nend\n\nfunction sortById(a, b)\n local numA = tonumber(a.id)\n local numB = tonumber(b.id)\n\n if numA and numB then\n return numA \u003c numB\n else\n return a.name \u003c b.name\n end\nend\n\nfunction none() end\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Token", @@ -202822,9 +144324,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -202838,6 +144340,13 @@ "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2342503777940937086/92256BDF101E6272AD1E3F5F0043D311DF708F03/", "WidthScale": 0 }, + "CustomUIAssets": [ + { + "Name": "OtherCards", + "Type": 0, + "URL": "http://cloud-3.steamusercontent.com/ugc/2446096169989812196/B5C491331EB348C261F561DC7A19968ECF9FC74A/" + } + ], "Description": "", "DragSelectable": true, "GMNotes": "", @@ -202849,7 +144358,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id table String ID of the card to retrieve\n ---@return table table\n -- If the indexes are still being constructed, an empty table is\n -- returned. Otherwise, a single table with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", {id = id})\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned\n -- it will be removed from the list and cannot be selected again until a reload\n -- occurs or the indexes are rebuilt, which will refresh the list to include all\n -- weaknesses.\n ---@return string: ID of the selected weakness.\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n return getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list. Note that this is\n -- an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return getAllCardsBag().call(\"getCardsByName\", {name = name, exact = exact})\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean true for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- cardData: TTS object data, suitable for spawning the card\n -- cardMetadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return getAllCardsBag().call(\"getCardsByClassAndLevel\", {class = class, upgraded = upgraded})\n end\n\n AllCardsBagApi.getCardsByCycle = function(cycle)\n return getAllCardsBag().call(\"getCardsByCycle\", cycle)\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return getAllCardsBag().call(\"getUniqueWeaknesses\")\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"playercards/PlayerCardPanelData\", function(require, _LOADED, __bundle_register, __bundle_modules)\nBONDED_CARD_LIST = {\n\t\"05314\", -- Soothing Melody\n\t\"06277\", -- Wish Eater\n\t\"06019\", -- Bloodlust\n\t\"06022\", -- Pendant of the Queen\n\t\"05317\", -- Blood-rite\n\t\"06113\", -- Essence of the Dream\n\t\"06028\", -- Stars Are Right\n\t\"06025\", -- Guardian of the Crystallizer\n\t\"06283\", -- Unbound Beast\n\t\"06032\", -- Zeal\n\t\"06031\", -- Hope\n\t\"06033\", -- Augur\n\t\"06331\", -- Dream Parasite\n\t\"06015a\", -- Dream-Gate\n\t\"10006\",\t\t-- Aetheric Current (Yuggoth)\n\t\"10007\",\t\t-- Aetheric Current (Yoth)\n\t\"10036\",\t\t-- Blade of Yoth\n\t\"10039\",\t\t-- Evanescent Ascension\n\t\"10045\", -- Uncanny Growth\n\t\"10063\",\t\t-- Bianca\n\t\"10086\",\t\t-- Rot\n\t\"10087\",\t\t-- Rot\n\t\"10088\",\t\t-- Rot\n\t\"10089\",\t\t-- Rot\n\t\"10090\",\t\t-- Rot\n\t\"10106\",\t\t-- Keeper of the Key\n\t\"10107\",\t\t-- Servant of Brass\n\t\"10134\",\t\t-- Twilight Diadem\n}\n\nUPGRADE_SHEET_LIST = {\n\t\"09040-c\", -- Alchemical Distillation\n\t\"09023-c\", -- Custom Modifications\n\t\"09059-c\", -- Damning Testimony\n\t\"09041-c\", -- Emperical Hypothesis\n\t\"09060-c\", -- Friends in Low Places\n\t\"09101-c\", -- Grizzled\n\t\"09061-c\", -- Honed Instinct\n\t\"09021-c\", -- Hunter's Armor\n\t\"09119-c\", -- Hyperphysical Shotcaster\n\t\"09079-c\", -- Living Ink\n\t\"09100-c\", -- Makeshift Trap\n\t\"09099-c\", -- Pocket Multi Tool\n\t\"09081-c\", -- Power Word\n\t\"09081-t-c\", -- Power Word (Taboo)\n\t\"09022-c\", -- Runic Axe\n\t\"09022-t-c\", -- Runic Axe (Taboo)\n\t\"09080-c\", -- Summoned Servitor\n\t\"09042-c\", -- Raven's Quill\n}\n\nEVOLVED_WEAKNESSES = {\n\t\"04039\",\n\t\"04041\",\n\t\"04042\",\n\t\"53014\",\n\t\"53015\",\n}\n\n------------------ START INVESTIGATOR DATA DEFINITION ------------------\nINVESTIGATOR_GROUPS = {\n [\"Guardian\"] = {\n \"Roland Banks\",\n \t\"Zoey Samaras\",\n \t\"Mark Harrigan\",\n \t\"Leo Anderson\",\n \t\"Carolyn Fern\",\n \t\"Tommy Muldoon\",\n \t\"Nathaniel Cho\",\n \t\"Sister Mary\",\n \t\"Daniela Reyes\",\n \t\"Carson Sinclair\",\n\t\t\"Wilson Richards\"\n },\n [\"Seeker\"] = {\n \"Daisy Walker\",\n \t\"Rex Murphy\",\n \t\"Minh Thi Phan\",\n \t\"Ursula Downs\",\n \t\"Joe Diamond\",\n \t\"Mandy Thompson\",\n \t\"Harvey Walters\",\n \t\"Amanda Sharpe\",\n \t\"Norman Withers\",\n \t\"Vincent Lee\",\n\t\t\"Kate Winthrop\"\n },\n [\"Rogue\"] = {\n \t\"\\\"Skids\\\" O'Toole\",\n \t\"Jenny Barnes\",\n \t\"Sefina Rousseau\",\n \t\"Finn Edwards\",\n \t\"Preston Fairmont\",\n \t\"Tony Morgan\",\n \t\"Winifred Habbamock\",\n \t\"Trish Scarborough\",\n \t\"Monterey Jack\",\n \t\"Kymani Jones\",\n\t\t\"Alessandra Zorzi\"\n },\n [\"Mystic\"] = {\n \t\"Agnes Baker\",\n \t\"Jim Culver\",\n \t\"Akachi Onyele\",\n \t\"Father Mateo\",\n \t\"Diana Stanley\",\n \t\"Marie Lambeau\",\n \t\"Luke Robinson\",\n \t\"Jacqueline Fine\",\n \t\"Dexter Drake\",\n \t\"Lily Chen\",\n \t\"Amina Zidane\",\n \t\"Gloria Goldberg\",\n\t\t\"Kōhaku Narukami\"\n },\n [\"Survivor\"] = {\n \t\"Wendy Adams\",\n \t\"\\\"Ashcan\\\" Pete\",\n \t\"William Yorick\",\n \t\"Calvin Wright\",\n \t\"Rita Young\",\n \t\"Patrice Hathaway\",\n \t\"Stella Clark\",\n \t\"Silas Marsh\",\n \t\"Bob Jenkins\",\n \t\"Darrell Simmons\",\n\t\t\"Hank Samson\"\n },\n [\"Neutral\"] = {\n \t\"Lola Hayes\",\n \t\"Charlie Kane\",\n \t\"Subject 5U-21\"\n },\n [\"Core\"] = {\n \"Roland Banks\",\n \"Daisy Walker\",\n \"\\\"Skids\\\" O'Toole\",\n \"Agnes Baker\",\n \"Wendy Adams\"\n },\n [\"The Dunwich Legacy\"] = {\n \t\"Zoey Samaras\",\n \t\"Rex Murphy\",\n \t\"Jenny Barnes\",\n \t\"Jim Culver\",\n \t\"\\\"Ashcan\\\" Pete\"\n },\n [\"The Path to Carcosa\"] = {\n \t\"Mark Harrigan\",\n \t\"Minh Thi Phan\",\n \t\"Sefina Rousseau\",\n \t\"Akachi Onyele\",\n \t\"William Yorick\",\n \t\"Lola Hayes\"\n },\n [\"The Forgotten Age\"] = {\n \t\"Leo Anderson\",\n \t\"Ursula Downs\",\n \t\"Finn Edwards\",\n \t\"Father Mateo\",\n \t\"Calvin Wright\"\n },\n [\"The Circle Undone\"] = {\n \t\"Carolyn Fern\",\n \t\"Joe Diamond\",\n \t\"Preston Fairmont\",\n \t\"Diana Stanley\",\n \t\"Rita Young\",\n \t\"Marie Lambeau\"\n },\n [\"The Dream-Eaters\"] = {\n \t\"Tommy Muldoon\",\n \t\"Mandy Thompson\",\n \t\"Tony Morgan\",\n \t\"Luke Robinson\",\n \t\"Patrice Hathaway\"\n },\n [\"Investigator Packs\"] = {\n \t\"Nathaniel Cho\",\n \t\"Harvey Walters\",\n \t\"Winifred Habbamock\",\n \t\"Jacqueline Fine\",\n \t\"Stella Clark\",\n \t\"Gloria Goldberg\"\n },\n [\"The Innsmouth Conspiracy\"] = {\n \t\"Sister Mary\",\n \t\"Amanda Sharpe\",\n \t\"Trish Scarborough\",\n \t\"Dexter Drake\",\n \t\"Silas Marsh\"\n },\n [\"Edge of the Earth\"] = {\n \t\"Daniela Reyes\",\n \t\"Norman Withers\",\n \t\"Monterey Jack\",\n \t\"Lily Chen\",\n \t\"Bob Jenkins\"\n },\n [\"The Scarlet Keys\"] = {\n \t\"Carson Sinclair\",\n \t\"Vincent Lee\",\n \t\"Kymani Jones\",\n \t\"Amina Zidane\",\n \t\"Darrell Simmons\",\n \t\"Charlie Kane\"\n },\n\t[\"The Feast of Hemlock Vale\"] = {\n\t\t\"Wilson Richards\",\n\t\t\"Kate Winthrop\",\n\t\t\"Alessandra Zorzi\",\n\t\t\"Kōhaku Narukami\",\n\t\t\"Hank Samson\"\n\t}\n}\n\nINVESTIGATORS = {}\n-- Core Box\nINVESTIGATORS[\"Roland Banks\"] = {\n\tcards = { \"01001\", \"01001-p\", \"01001-pf\", \"01001-pb\" },\n\tminicards = { \"01001-m\" },\n\tsignatures = { \"01006\", \"01007\", \"90030\", \"90031\", \"90025\", \"90026\", \"90027\", \"90028\", \"90029\", \"98005\", \"98006\" },\n\tstarterDeck = \"2624931\"\n}\nINVESTIGATORS[\"Daisy Walker\"] = {\n\tcards = { \"01002\", \"01002-p\", \"01002-pf\", \"01002-pb\" },\n\tminicards = { \"01002-m\" },\n\tsignatures = { \"01008\", \"01009\", \"90002\", \"90003\" },\n\tstarterDeck = \"2624938\"\n}\nINVESTIGATORS[\"\\\"Skids\\\" O'Toole\"] = {\n\tcards = { \"01003\", \"01003-p\", \"01003-pf\", \"01003-pb\" },\n\tminicards = { \"01003-m\" },\n\tsignatures = { \"01010\", \"01011\", \"90009\", \"90010\" },\n\tstarterDeck = \"2624940\"\n}\nINVESTIGATORS[\"Agnes Baker\"] = {\n\tcards = { \"01004\", \"01004-p\", \"01004-pf\", \"01004-pb\" },\n\tminicards = { \"01004-m\" },\n\tsignatures = { \"01012\", \"01013\", \"90018\", \"90019\" },\n\tstarterDeck = \"2624944\"\n}\nINVESTIGATORS[\"Wendy Adams\"] = {\n\tcards = { \"01005\", \"01005-p\", \"01005-pf\", \"01005-pb\" },\n\tminicards = { \"01005-m\" },\n\tsignatures = { \"01014\", \"01015\", \"01515\", \"90039\", \"90040\", \"90038\" },\n\tstarterDeck = \"2624949\"\n}\n-- The Dunwich Legacy\nINVESTIGATORS[\"Zoey Samaras\"] = {\n\tcards = { \"02001\", \"02001-p\", \"02001-pf\", \"02001-pb\" },\n\tminicards = { \"02001-m\" },\n\tsignatures = { \"02006\", \"02007\", \"90060\", \"90061\" },\n\tstarterDeck = \"2624950\"\n}\nINVESTIGATORS[\"Rex Murphy\"] = {\n\tcards = { \"02002\", \"02002-t\", \"02002-p\", \"02002-pf\", \"02002-pb\" },\n\tminicards = { \"02002-m\" },\n\tsignatures = { \"02008\", \"02009\", \"90079\", \"90080\" },\n\tstarterDeck = \"2624958\"\n}\nINVESTIGATORS[\"Jenny Barnes\"] = {\n\tcards = { \"02003\" },\n\tminicards = { \"02003-m\" },\n\tsignatures = { \"02010\", \"02011\", \"98002\", \"98003\" },\n\tstarterDeck = \"2624961\"\n}\nINVESTIGATORS[\"Jim Culver\"] = {\n\tcards = { \"02004\", \"02004-p\", \"02004-pf\", \"02004-pb\" },\n\tminicards = { \"02004-m\" },\n\tsignatures = { \"02012\", \"02013\", \"90050\", \"90051\", \"90052\", \"90053\" },\n\tstarterDeck = \"2624965\"\n}\nINVESTIGATORS[\"\\\"Ashcan\\\" Pete\"] = {\n\tcards = { \"02005\", \"02005-p\", \"02005-pf\", \"02005-pb\" },\n\tminicards = { \"02005-m\" },\n\tsignatures = { \"02014\", \"02015\", \"90047\", \"90048\" },\n\tstarterDeck = \"2624969\"\n}\n-- The Path to Carcosa\nINVESTIGATORS[\"Mark Harrigan\"] = {\n\tcards = { \"03001\" },\n\tminicards = { \"03001-m\" },\n\tsignatures = { \"03007\", \"03008\", \"03009\" },\n\tstarterDeck = \"2624975\"\n}\nINVESTIGATORS[\"Minh Thi Phan\"] = {\n\tcards = { \"03002\" },\n\tminicards = { \"03002-m\" },\n\tsignatures = { \"03010\", \"03011\" },\n\tstarterDeck = \"2624979\"\n}\nINVESTIGATORS[\"Sefina Rousseau\"] = {\n\tcards = { \"03003\" },\n\tminicards = { \"03003-m\" },\n\tsignatures = { \"03012\", \"03012\", \"03012\", \"03013\" },\n\tstarterDeck = \"2624981\"\n}\nINVESTIGATORS[\"Akachi Onyele\"] = {\n\tcards = { \"03004\" },\n\tminicards = { \"03004-m\" },\n\tsignatures = { \"03014\", \"03015\" },\n\tstarterDeck = \"2624984\"\n}\nINVESTIGATORS[\"William Yorick\"] = {\n\tcards = { \"03005\" },\n\tminicards = { \"03005-m\" },\n\tsignatures = { \"03016\", \"03017\" },\n\tstarterDeck = \"2624988\"\n}\nINVESTIGATORS[\"Lola Hayes\"] = {\n\tcards = { \"03006\", \"03006-t\" },\n\tminicards = { \"03006-m\" },\n\tsignatures = { \"03018\", \"03018\", \"03019\", \"03019\", \"03019-t\", \"03019-t\" },\n\tstarterDeck = \"2624990\"\n}\n-- The Forgotten Age\nINVESTIGATORS[\"Leo Anderson\"] = {\n\tcards = { \"04001\" },\n\tminicards = { \"04001-m\" },\n\tsignatures = { \"04006\", \"04007\" },\n\tstarterDeck = \"2624994\"\n}\nINVESTIGATORS[\"Ursula Downs\"] = {\n\tcards = { \"04002\" },\n\tminicards = { \"04002-m\" },\n\tsignatures = { \"04008\", \"04009\" },\n\tstarterDeck = \"2625000\"\n}\nINVESTIGATORS[\"Finn Edwards\"] = {\n\tcards = { \"04003\" },\n\tminicards = { \"04003-m\" },\n\tsignatures = { \"04010\", \"04011\", \"04012\" },\n\tstarterDeck = \"2625003\"\n}\nINVESTIGATORS[\"Father Mateo\"] = {\n\tcards = { \"04004\" },\n\tminicards = { \"04004-m\" },\n\tsignatures = { \"04013\", \"04014\" },\n\tstarterDeck = \"2625005\"\n}\nINVESTIGATORS[\"Calvin Wright\"] = {\n\tcards = { \"04005\" },\n\tminicards = { \"04005-m\" },\n\tsignatures = { \"04015\", \"04016\" },\n\tstarterDeck = \"2625007\"\n}\n-- The Circle Undone\nINVESTIGATORS[\"Carolyn Fern\"] = {\n\tcards = { \"05001\" },\n\tminicards = { \"05001-m\" },\n\tsignatures = { \"05007\", \"05008\", \"98011\", \"98012\" },\n\tstarterDeck = \"2626342\"\n}\nINVESTIGATORS[\"Joe Diamond\"] = {\n\tcards = { \"05002\" },\n\tminicards = { \"05002-m\" },\n\tsignatures = { \"05009\", \"05010\" },\n\tstarterDeck = \"2626347\"\n}\nINVESTIGATORS[\"Preston Fairmont\"] = {\n\tcards = { \"05003\" },\n\tminicards = { \"05003-m\" },\n\tsignatures = { \"05011\", \"05012\" },\n\tstarterDeck = \"2626365\"\n}\nINVESTIGATORS[\"Diana Stanley\"] = {\n\tcards = { \"05004\" },\n\tminicards = { \"05004-m\" },\n\tsignatures = { \"05013\", \"05014\", \"05015\" },\n\tstarterDeck = \"2626385\"\n}\nINVESTIGATORS[\"Rita Young\"] = {\n\tcards = { \"05005\" },\n\tminicards = { \"05005-m\" },\n\tsignatures = { \"05016\", \"05017\" },\n\tstarterDeck = \"2626387\"\n}\nINVESTIGATORS[\"Marie Lambeau\"] = {\n\tcards = { \"05006\" },\n\tminicards = { \"05006-m\" },\n\tsignatures = { \"05018\", \"05019\" },\n\tstarterDeck = \"2626395\"\n}\n-- The Dream-Eaters\nINVESTIGATORS[\"Tommy Muldoon\"] = {\n\tcards = { \"06001\" },\n\tminicards = { \"06001-m\" },\n\tsignatures = { \"06006\", \"06007\" },\n\tstarterDeck = \"2626402\"\n}\nINVESTIGATORS[\"Mandy Thompson\"] = {\n\tcards = { \"06002\", \"06002-t\" },\n\tminicards = { \"06002-m\" },\n\tsignatures = { \"06008\", \"06008\", \"06008\", \"06009\" },\n\tstarterDeck = \"2626410\"\n}\nINVESTIGATORS[\"Tony Morgan\"] = {\n\tcards = { \"06003\" },\n\tminicards = { \"06003-m\" },\n\tsignatures = { \"06010\", \"06011\", \"06011\", \"06012\" },\n\tstarterDeck = \"2626446\"\n}\nINVESTIGATORS[\"Luke Robinson\"] = {\n\tcards = { \"06004\" },\n\tminicards = { \"06004-m\" },\n\tsignatures = { \"06013\", \"06014\", \"06015\" },\n\tstarterDeck = \"2626452\"\n}\nINVESTIGATORS[\"Patrice Hathaway\"] = {\n\tcards = { \"06005\" },\n\tminicards = { \"06005-m\" },\n\tsignatures = { \"06016\", \"06017\" },\n\tstarterDeck = \"2626461\"\n}\n-- Starter Decks\nINVESTIGATORS[\"Nathaniel Cho\"] = {\n\tcards = { \"60101\" },\n\tminicards = { \"60101-m\" },\n\tsignatures = { \"60102\", \"60103\" },\n\tstarterDeck = \"2643925\"\n}\nINVESTIGATORS[\"Harvey Walters\"] = {\n\tcards = { \"60201\" },\n\tminicards = { \"60201-m\" },\n\tsignatures = { \"60202\", \"60203\" },\n\tstarterDeck = \"2643928\"\n}\nINVESTIGATORS[\"Winifred Habbamock\"] = {\n\tcards = { \"60301\" },\n\tminicards = { \"60301-m\" },\n\tsignatures = { \"60302\", \"60303\" },\n\tstarterDeck = \"2643931\"\n}\nINVESTIGATORS[\"Jacqueline Fine\"] = {\n\tcards = { \"60401\" },\n\tminicards = { \"60401-m\" },\n\tsignatures = { \"60402\", \"60403\" },\n\tstarterDeck = \"2643932\"\n}\nINVESTIGATORS[\"Stella Clark\"] = {\n\tcards = { \"60501\" },\n\tminicards = { \"60501-m\" },\n\tsignatures = { \"60502\", \"60502\", \"60502\", \"60503\" },\n\tstarterDeck = \"2643934\"\n}\n-- The Innsmouth Conspiracy\nINVESTIGATORS[\"Sister Mary\"] = {\n\tcards = { \"07001\" },\n\tminicards = { \"07001-m\" },\n\tsignatures = { \"07006\", \"07007\" },\n\tstarterDeck = \"2626464\"\n}\nINVESTIGATORS[\"Amanda Sharpe\"] = {\n\tcards = { \"07002\" },\n\tminicards = { \"07002-m\" },\n\tsignatures = { \"07008\", \"07009\" },\n\tstarterDeck = \"2626469\"\n}\nINVESTIGATORS[\"Trish Scarborough\"] = {\n\tcards = { \"07003\", \"07003-t\" },\n\tminicards = { \"07003-m\" },\n\tsignatures = { \"07010\", \"07011\" },\n\tstarterDeck = \"2626479\"\n}\nINVESTIGATORS[\"Dexter Drake\"] = {\n\tcards = { \"07004\" },\n\tminicards = { \"07004-m\" },\n\tsignatures = { \"07012\", \"07013\", \"98017\", \"98018\" },\n\tstarterDeck = \"2626672\"\n}\nINVESTIGATORS[\"Silas Marsh\"] = {\n\tcards = { \"07005\" },\n\tminicards = { \"07005-m\" },\n\tsignatures = { \"07014\", \"07015\", \"07016\", \"98014\", \"98015\" },\n\tstarterDeck = \"2626685\"\n}\n-- Edge of the Earth\nINVESTIGATORS[\"Daniela Reyes\"] = {\n\tcards = { \"08001\" },\n\tminicards = { \"08001-m\" },\n\tsignatures = { \"08002\", \"08003\" },\n\tstarterDeck = \"2634588\"\n}\nINVESTIGATORS[\"Norman Withers\"] = {\n\tcards = { \"08004\" },\n\tminicards = { \"08004-m\" },\n\tsignatures = { \"08005\", \"08006\", \"98008\", \"98009\" },\n\tstarterDeck = \"2634603\"\n}\nINVESTIGATORS[\"Monterey Jack\"] = {\n\tcards = { \"08007\", \"08007-p\", \"08007-pf\", \"08007-pb\" },\n\tminicards = { \"08007-m\" },\n\tsignatures = { \"08008\", \"08009\", \"90063\", \"90064\" },\n\tstarterDeck = \"2634652\"\n}\nINVESTIGATORS[\"Lily Chen\"] = {\n\tcards = { \"08010\" },\n\tminicards = { \"08010-m\" },\n\tsignatures = { \"08011a\", \"08012a\", \"08013a\", \"08014a\", \"08015\", \"08015\", \"08015\", \"08015\" },\n\tstarterDeck = \"2634658\"\n}\nINVESTIGATORS[\"Bob Jenkins\"] = {\n\tcards = { \"08016\" },\n\tminicards = { \"08016-m\" },\n\tsignatures = { \"08017\", \"08018\" },\n\tstarterDeck = \"2634643\"\n}\n-- The Scarlet Keys\nINVESTIGATORS[\"Carson Sinclair\"] = {\n\tcards = { \"09001\" },\n\tminicards = { \"09001-m\" },\n\tsignatures = { \"09002\", \"09002\", \"09003\" },\n\tstarterDeck = \"2634667\"\n}\nINVESTIGATORS[\"Vincent Lee\"] = {\n\tcards = { \"09004\" },\n\tminicards = { \"09004-m\" },\n\tsignatures = { \"09005\", \"09006\", \"09006\", \"09006\", \"09006\", \"09007\" },\n\tstarterDeck = \"2634675\"\n}\nINVESTIGATORS[\"Kymani Jones\"] = {\n\tcards = { \"09008\" },\n\tminicards = { \"09008-m\" },\n\tsignatures = { \"09009\", \"09010\" },\n\tstarterDeck = \"2634701\"\n}\nINVESTIGATORS[\"Amina Zidane\"] = {\n\tcards = { \"09011\" },\n\tminicards = { \"09011-m\" },\n\tsignatures = { \"09012\", \"09013\", \"09014\" },\n\tstarterDeck = \"2634697\"\n}\nINVESTIGATORS[\"Darrell Simmons\"] = {\n\tcards = { \"09015\" },\n\tminicards = { \"09015-m\" },\n\tsignatures = { \"09016\", \"09017\" },\n\tstarterDeck = \"2634757\"\n}\nINVESTIGATORS[\"Charlie Kane\"] = {\n\tcards = { \"09018\" },\n\tminicards = { \"09018-m\" },\n\tsignatures = { \"09019\", \"09020\" },\n\tstarterDeck = \"2634706\"\n}\n-- The Feast of Hemlock Vale\nINVESTIGATORS[\"Wilson Richards\"] = {\n\tcards = { \"10001\" },\n\tminicards = { \"10001-m\" },\n\tsignatures = { \"10002\", \"10003\" },\n\tstarterDeck = \"2634667\" --carson deck as placeholder\n}\nINVESTIGATORS[\"Kate Winthrop\"] = {\n\tcards = { \"10004\" },\n\tminicards = { \"10004-m\" },\n\tsignatures = { \"10005\", \"10006\", \"10007\", \"10008\" },\n\tstarterDeck = \"2643928\" --harvey deck as placeholder\n}\nINVESTIGATORS[\"Alessandra Zorzi\"] = {\n\tcards = { \"10009\" },\n\tminicards = { \"10009-m\" },\n\tsignatures = { \"10010\", \"10010\", \"10010\", \"10011\" },\n\tstarterDeck = \"2643931\" --winifred deck as placeholder\n}\nINVESTIGATORS[\"Kōhaku Narukami\"] = {\n\tcards = { \"10012\" },\n\tminicards = { \"10012-m\" },\n\tsignatures = { \"10013\", \"10014\" },\n\tstarterDeck = \"2636199\" --gloria deck as placeholder\n}\nINVESTIGATORS[\"Hank Samson\"] = {\n\tcards = { \"10015\", \"10015-b1\", \"10015-b2\" },\n\tminicards = { \"10015-m\" },\n\tsignatures = { \"10017\", \"10018\"},\n\tstarterDeck = \"2643934\" --stella deck as placeholder\n}\n-- PnP content\nINVESTIGATORS[\"Subject 5U-21\"] = {\n\tcards = { \"89001\" },\n\tminicards = { \"89001-m\" },\n\tsignatures = { \"89002\", \"89003\", \"89003\", \"89003\", \"89004\", \"89004\", \"89004\", \"89005\" },\n\tstarterDeck = \"2624990\" -- Lola's deck id until Suzi is on ArkhamDB\n}\n-- Promo content\nINVESTIGATORS[\"Gloria Goldberg\"] = {\n\tcards = { \"98019\" },\n\tminicards = { \"98019-m\" },\n\tsignatures = { \"98020\", \"98021\" },\n\tstarterDeck = \"2636199\"\n}\n------------------ END INVESTIGATOR DATA DEFINITION ------------------\nend)\n__bundle_register(\"playercards/PlayerCardSpawner\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Amount to shift for the next card (zShift) or next row of cards (xShift)\n-- Note that the table rotation is weird, and the X axis is vertical while the\n-- Z axis is horizontal\nlocal SPREAD_Z_SHIFT = -2.3\nlocal SPREAD_X_SHIFT = -3.66\n\nSpawner = { }\n\n-- Spawns a list of cards at the given position/rotation. This will separate cards by size -\n-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If\n-- there are different types, the provided callback will be called once for each type as it spawns\n-- either a card or deck.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos tts__Vector table where the cards should be spawned (global)\n---@param rot tts__Vector table for the orientation of the spawned cards (global)\n---@param sort boolean True if this list of cards should be sorted before spawning\n---@param callback? function Callback to be called after the card/deck spawns.\nSpawner.spawnCards = function(cardList, pos, rot, sort, callback)\n if (sort) then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local miniCards = { }\n local standardCards = { }\n local investigatorCards = { }\n\n for _, card in ipairs(cardList) do\n if (card.metadata.type == \"Investigator\") then\n table.insert(investigatorCards, card)\n elseif (card.metadata.type == \"Minicard\") then\n table.insert(miniCards, card)\n else\n table.insert(standardCards, card)\n end\n end\n -- Spawn each of the three types individually. Each Y position shift accounts for the thickness\n -- of the spawned deck\n local position = { x = pos.x, y = pos.y, z = pos.z }\n Spawner.spawn(investigatorCards, position, rot, callback)\n\n position.y = position.y + (#investigatorCards + #standardCards) * 0.07\n Spawner.spawn(standardCards, position, rot, callback)\n\n position.y = position.y + (#standardCards + #miniCards) * 0.07\n Spawner.spawn(miniCards, position, rot, callback)\nend\n\nSpawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)\n if (sort) then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local position = { x = startPos.x, y = startPos.y, z = startPos.z }\n -- Special handle the first row if we have less than a full single row, but only if there's a\n -- reasonable max column count. Single-row spreads will send a large value for maxCols\n if maxCols \u003c 100 and #cardList \u003c maxCols then\n position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)\n end\n local cardsInRow = 0\n local rows = 0\n for _, card in ipairs(cardList) do\n Spawner.spawn({ card }, position, rot, callback)\n position.z = position.z + SPREAD_Z_SHIFT\n cardsInRow = cardsInRow + 1\n if cardsInRow \u003e= maxCols then\n rows = rows + 1\n local cardsForRow = #cardList - rows * maxCols\n if cardsForRow \u003e maxCols then\n cardsForRow = maxCols\n end\n position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)\n position.x = position.x + SPREAD_X_SHIFT\n cardsInRow = 0\n end\n end\nend\n\n-- Spawn a specific list of cards. This method is for internal use and should not be called\n-- directly, use spawnCards instead.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos table Position where the cards should be spawned (global)\n---@param rot table Rotation for the orientation of the spawned cards (global)\n---@param callback? function callback to be called after the card/deck spawns.\nSpawner.spawn = function(cardList, pos, rot, callback)\n if #cardList == 0 then return end\n\n -- Spawn a single card directly\n if #cardList == 1 then\n -- handle sideways card\n if cardList[1].data.SidewaysCard then\n rot = { rot.x, rot.y - 90, rot.z }\n end\n spawnObjectData({\n data = cardList[1].data,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\n return\n end\n\n -- For multiple cards, construct a deck and spawn that\n local deck = Spawner.buildDeckDataTemplate()\n\n -- Decks won't inherently scale to the cards in them. The card list being spawned should be all\n -- the same type/size by this point, so use the first card to set the size\n deck.Transform = {\n scaleX = cardList[1].data.Transform.scaleX,\n scaleY = 1,\n scaleZ = cardList[1].data.Transform.scaleZ\n }\n\n local sidewaysDeck = true\n for _, spawnCard in ipairs(cardList) do\n Spawner.addCardToDeck(deck, spawnCard.data)\n -- set sidewaysDeck to false if any card is not a sideways card\n sidewaysDeck = (sidewaysDeck and spawnCard.data.SidewaysCard)\n end\n\n -- set the alt view angle for sideways decks\n if sidewaysDeck then\n deck.AltLookAngle = { x = 0, y = 180, z = 90 }\n rot = { rot.x, rot.y - 90, rot.z }\n end\n\n spawnObjectData({\n data = deck,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\nend\n\n-- Inserts a card into the given deck. This does three things:\n-- 1. Add the card's data to ContainedObjects\n-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's\n-- ID list. Note that the deck's ID list is \"DeckIDs\" even though it\n-- contains a list of card Ids\n-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's\n-- \"CustomDeck\" field is a list of all CustomDecks used by cards within the\n-- deck, keyed by the DeckID and referencing the custom deck table\n---@param deck table TTS deck data structure to add to\n---@param cardData table Data for the card to be inserted\nSpawner.addCardToDeck = function(deck, cardData)\n for customDeckId, customDeckData in pairs(cardData.CustomDeck) do\n if (deck.CustomDeck[customDeckId] == nil) then\n -- CustomDeck not added to deck yet, add it\n deck.CustomDeck[customDeckId] = customDeckData\n elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then\n -- CustomDeck for this card matches the current one for the deck, do nothing\n else\n -- CustomDeck data conflict\n local newDeckId = nil\n for deckId, customDeck in pairs(deck.CustomDeck) do\n if (customDeckData.FaceURL == customDeck.FaceURL) then\n newDeckId = deckId\n end\n end\n if (newDeckId == nil) then\n -- No non-conflicting custom deck for this card, add a new one\n newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, \"1000\")\n deck.CustomDeck[newDeckId] = customDeckData\n end\n -- Update the card with the new CustomDeck info\n cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)\n cardData.CustomDeck[customDeckId] = nil\n cardData.CustomDeck[newDeckId] = customDeckData\n break\n end\n end\n table.insert(deck.ContainedObjects, cardData)\n table.insert(deck.DeckIDs, cardData.CardID)\nend\n\n-- Create an empty deck data table which can have cards added to it. This\n-- creates a new table on each call without using metatables or previous\n-- definitions because we can't be sure that TTS doesn't modify the structure\n---@return table deck Table containing the minimal TTS deck data structure\nSpawner.buildDeckDataTemplate = function()\n local deck = {}\n deck.Name = \"Deck\"\n\n -- Card data. DeckIDs and CustomDeck entries will be built from the cards\n deck.ContainedObjects = {}\n deck.DeckIDs = {}\n deck.CustomDeck = {}\n\n -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here\n deck.Transform = {\n scaleX = 1,\n scaleY = 1,\n scaleZ = 1,\n }\n\n return deck\nend\n\n-- Returns the first ID which does not exist in the given table, starting at startId and increasing\n---@param objectTable table keyed by strings which are numbers\n---@param startId string possible ID.\n---@return string id \u003e= startId\nSpawner.findNextAvailableId = function(objectTable, startId)\n local id = startId\n while (objectTable[id] ~= nil) do\n id = tostring(tonumber(id) + 1)\n end\n return id\nend\n\n-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.\n---@return number PBCN 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are\n-- irrelevant as they provide only grouping and the order between them doesn't matter.\nSpawner.getpbcn = function(metadata)\n if metadata.permanent then\n return 1\n elseif metadata.bonded_to ~= nil then\n return 2\n else -- Normal card\n return 3\n end\nend\n\n-- Comparison function used to sort the cards in a deck. Groups bonded or\n-- permanent cards first, then sorts within theose types by name/subname.\n-- Normal cards will sort in standard alphabetical order, while\n-- permanent/bonded/customizable will be in reverse alphabetical order.\n--\n-- Since cards spawn in the order provided by this comparator, with the first\n-- cards ending up at the bottom of a pile, this ordering will spawn in reverse\n-- alphabetical order. This presents the cards in order for non-face-down\n-- areas, and presents them in order when Searching the face-down deck.\nSpawner.cardComparator = function(card1, card2)\n local pbcn1 = Spawner.getpbcn(card1.metadata)\n local pbcn2 = Spawner.getpbcn(card2.metadata)\n if pbcn1 ~= pbcn2 then\n return pbcn1 \u003e pbcn2\n end\n if pbcn1 == 3 then\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n end\n return card1.data.Description \u003c card2.data.Description\n else\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003e card2.data.Nickname\n end\n return card1.data.Description \u003e card2.data.Description\n end\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/PlayerCardPanel\")\nend)\n__bundle_register(\"arkhamdb/ArkhamDb\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n\n local ArkhamDb = {}\n local internal = {}\n \n local tabooList = {}\n local configuration\n\n local RANDOM_WEAKNESS_ID = \"01000\"\n\n ---@class Request\n local Request = {}\n\n -- Sets up the ArkhamDb interface. Should be called from the parent object on load.\n ArkhamDb.initialize = function()\n configuration = internal.getConfiguration()\n Request.start({ configuration.api_uri, configuration.taboo }, function(status)\n local json = JSON.decode(internal.fixUtf16String(status.text))\n for _, taboo in pairs(json) do\n local cards = {}\n\n for _, card in pairs(JSON.decode(taboo.cards)) do\n cards[card.code] = true\n end\n\n tabooList[taboo.id] = {\n date = taboo.date_start,\n cards = cards\n }\n end\n return true, nil\n end)\n end\n\n -- Start the deck build process for the given player color and deck ID. This\n -- will retrieve the deck from ArkhamDB, and pass to a callback for processing.\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\").\n ---@param deckId string ArkhamDB deck id to be loaded\n ---@param isPrivate boolean Whether this deck is published or private on ArkhamDB\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n ---@return boolean\n ---@return string\n ArkhamDb.getDecklist = function(\n playerColor,\n deckId,\n isPrivate,\n loadNewest,\n loadInvestigators,\n callback)\n -- Get a simple card to see if the bag indexes are complete. If not, abort\n -- the deck load. The called method will handle player notification.\n local checkCard = allCardsBagApi.getCardById(\"01001\")\n if (checkCard ~= nil and checkCard.data == nil) then\n return false, \"Indexing not complete\"\n end\n\n local deckUri = {\n configuration.api_uri,\n isPrivate and configuration.private_deck or configuration.public_deck,\n deckId\n }\n\n local deck = Request.start(deckUri, function(status)\n if string.find(status.text, \"\u003c!DOCTYPE html\u003e\") then\n internal.maybePrint(\"Private deck ID \" .. deckId .. \" is not shared\", playerColor)\n return false, \"Private deck \" .. deckId .. \" is not shared\"\n end\n local json = JSON.decode(status.text)\n\n if not json then\n internal.maybePrint(\"Deck ID \" .. deckId .. \" not found\", playerColor)\n return false, \"Deck not found!\"\n end\n\n return true, json\n end)\n\n deck:with(internal.onDeckResult, playerColor, loadNewest, loadInvestigators, callback)\n end\n\n -- Logs that a card could not be loaded in the mod by printing it to the console in the given\n -- color of the player owning the deck. Attempts to look up the name on ArkhamDB for clarity,\n -- but prints the card ID if the name cannot be retrieved.\n ---@param cardId string ArkhamDB ID of the card that could not be found\n ---@param playerColor string Color of the player's deck that had the problem\n ArkhamDb.logCardNotFound = function(cardId, playerColor)\n local request = Request.start({\n configuration.api_uri,\n configuration.cards,\n cardId\n },\n function(result)\n local adbCardInfo = JSON.decode(internal.fixUtf16String(result.text))\n local cardName = adbCardInfo.real_name\n if (cardName ~= nil) then\n if (adbCardInfo.xp ~= nil and adbCardInfo.xp \u003e 0) then\n cardName = cardName .. \" (\" .. adbCardInfo.xp .. \")\"\n end\n internal.maybePrint(\"Card not found: \" .. cardName .. \", card ID \" .. cardId, playerColor)\n else\n internal.maybePrint(\"Card not found in ArkhamDB/Index, ID \" .. cardId, playerColor)\n end\n end)\n end\n\n -- Callback when the deck information is received from ArkhamDB. Parses the\n -- response then applies standard transformations to the deck such as adding\n -- random weaknesses and checking for taboos. Once the deck is processed,\n -- passes to loadCards to actually spawn the defined deck.\n ---@param deck table ArkhamImportDeck\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\")\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load.\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- bondedList A table of cardID keys to meaningless values. Card IDs in this list were\n --- added from a parent bonded card.\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n internal.onDeckResult = function(deck, playerColor, loadNewest, loadInvestigators, callback)\n -- Load the next deck in the upgrade path if the option is enabled\n if (loadNewest and deck.next_deck ~= nil and deck.next_deck ~= \"\") then\n buildDeck(playerColor, deck.next_deck)\n return\n end\n\n internal.maybePrint(table.concat({ \"Found decklist: \", deck.name }), playerColor)\n\n -- Initialize deck slot table and perform common transformations. The order of these should not\n -- be changed, as later steps may act on cards added in each. For example, a random weakness or\n -- investigator may have bonded cards or taboo entries, and should be present\n local slots = deck.slots\n internal.maybeDrawRandomWeakness(slots, playerColor)\n\n -- handles alternative investigators (parallel, promo or revised art)\n local loadAltInvestigator = \"normal\"\n if loadInvestigators then\n loadAltInvestigator = internal.addInvestigatorCards(deck, slots)\n end\n\n internal.maybeModifyDeckFromDescription(slots, deck.description_md, playerColor)\n internal.maybeAddSummonedServitor(slots)\n internal.maybeAddOnTheMend(slots, playerColor)\n internal.maybeAddRealityAcidReference(slots)\n local bondList = internal.extractBondedCards(slots)\n internal.checkTaboos(deck.taboo_id, slots, playerColor)\n internal.maybeAddUpgradeSheets(slots)\n\n -- get upgrades for customizable cards\n local customizations = {}\n if deck.meta then\n customizations = JSON.decode(deck.meta)\n end\n\n callback(slots, deck.investigator_code, bondList, customizations, playerColor, loadAltInvestigator)\n end\n\n -- Checks to see if the slot list includes the random weakness ID. If it does,\n -- removes it from the deck and replaces it with the ID of a random basic weakness provided by the\n -- all cards bag\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n --- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast\n --- if a weakness is added.\n internal.maybeDrawRandomWeakness = function(slots, playerColor)\n local randomWeaknessAmount = slots[RANDOM_WEAKNESS_ID] or 0\n slots[RANDOM_WEAKNESS_ID] = nil\n\n if randomWeaknessAmount ~= 0 then\n for i=1, randomWeaknessAmount do\n local weaknessId = allCardsBagApi.getRandomWeaknessId()\n slots[weaknessId] = (slots[weaknessId] or 0) + 1\n end\n internal.maybePrint(\"Added \" .. randomWeaknessAmount .. \" random basic weakness(es) to deck\", playerColor)\n end\n end\n\n -- Adds both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each\n ---@param deck table The processed ArkhamDB deck response\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the\n --- number of those cards which will be spawned\n ---@return string: Contains the name of the art that should be loaded (\"normal\", \"promo\" or \"revised\")\n internal.addInvestigatorCards = function(deck, slots)\n local investigatorId = deck.investigator_code\n slots[investigatorId .. \"-m\"] = 1\n local deckMeta = JSON.decode(deck.meta)\n\n -- handling alternative investigator art and parallel investigators\n local loadAltInvestigator = \"normal\"\n if deckMeta ~= nil then\n local altFrontId = tonumber(deckMeta.alternate_front) or 0\n local altBackId = tonumber(deckMeta.alternate_back) or 0\n local altArt = { front = \"normal\", back = \"normal\" }\n\n -- translating front ID\n if altFrontId \u003e 90000 and altFrontId \u003c 90100 then\n altArt.front = \"parallel\"\n elseif altFrontId \u003e 01500 and altFrontId \u003c 01506 then\n altArt.front = \"revised\"\n elseif altFrontId \u003e 98000 then\n altArt.front = \"promo\"\n end\n\n -- translating back ID\n if altBackId \u003e 90000 and altBackId \u003c 90100 then\n altArt.back = \"parallel\"\n elseif altBackId \u003e 01500 and altBackId \u003c 01506 then\n altArt.back = \"revised\"\n elseif altBackId \u003e 98000 then\n altArt.back = \"promo\"\n end\n\n -- updating investigatorID based on alt investigator selection\n -- precedence: parallel \u003e promo \u003e revised\n if altArt.front == \"parallel\" then\n if altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-p\"\n else\n investigatorId = investigatorId .. \"-pf\"\n end\n elseif altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-pb\"\n elseif altArt.front == \"promo\" or altArt.back == \"promo\" then\n loadAltInvestigator = \"promo\"\n elseif altArt.front == \"revised\" or altArt.back == \"revised\" then\n loadAltInvestigator = \"revised\"\n end\n end\n slots[investigatorId] = 1\n deck.investigator_code = investigatorId\n return loadAltInvestigator\n end\n\n -- Process the card list looking for the customizable cards, and add their upgrade sheets if needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddUpgradeSheets = function(slots)\n for cardId, _ in pairs(slots) do\n -- upgrade sheets for customizable cards\n local upgradesheet = allCardsBagApi.getCardById(cardId .. \"-c\")\n if upgradesheet ~= nil then\n slots[cardId .. \"-c\"] = 1\n end\n end\n end\n\n -- Process the card list looking for the Summoned Servitor, and add its minicard to the list if\n -- needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddSummonedServitor = function(slots)\n if slots[\"09080\"] ~= nil then\n slots[\"09080-m\"] = 1\n end\n end\n\n -- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1. Update\n -- the count based on the investigator count\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast if an error occurs\n internal.maybeAddOnTheMend = function(slots, playerColor)\n if slots[\"09006\"] ~= nil then\n local investigatorCount = playAreaApi.getInvestigatorCount()\n if investigatorCount ~= nil then\n slots[\"09006\"] = investigatorCount\n else\n internal.maybePrint(\"Something went wrong with the load, adding 4 copies of On the Mend\", playerColor)\n slots[\"09006\"] = 4\n end\n end\n end\n\n -- Process the card list looking for Reality Acid and adds the reference sheet when needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddRealityAcidReference = function(slots)\n if slots[\"89004\"] ~= nil then\n slots[\"89005\"] = 1\n end\n end\n\n -- Processes the deck description from ArkhamDB and modifies the slot list accordingly\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n ---@param description string The deck desription from ArkhamDB\n internal.maybeModifyDeckFromDescription = function(slots, description, playerColor)\n -- check for import instructions\n local pos = string.find(description, \"++SCED import instructions++\")\n if not pos then return end\n\n -- remove everything before instructions\n local tempStr = string.sub(description, pos)\n\n -- parse each line in instructions\n for line in tempStr:gmatch(\"([^\\n]+)\") do\n -- remove dashes at the start\n line = line:gsub(\"%- \", \"\")\n\n -- remove spaces\n line = line:gsub(\"%s\", \"\")\n\n -- remove balanced brackets\n line = line:gsub(\"%b()\", \"\")\n line = line:gsub(\"%b[]\", \"\")\n\n -- get instructor\n local instructor = \"\"\n for word in line:gmatch(\"%a+:\") do\n instructor = word\n break\n end\n\n -- go to the next line if no valid instructor found\n if instructor ~= \"add:\" and instructor ~= \"remove:\" then\n goto nextLine\n end\n\n -- remove instructor from line\n line = line:gsub(instructor, \"\")\n\n -- evaluate instructions\n for str in line:gmatch(\"([^,]+)\") do\n if instructor == \"add:\" then\n slots[str] = (slots[str] or 0) + 1\n elseif instructor == \"remove:\" then\n if slots[str] == nil then\n internal.maybePrint(\"Tried to remove card ID \" .. str .. \", but didn't find card in deck.\", playerColor)\n else\n slots[str] = math.max(slots[str] - 1, 0)\n\n -- fully remove cards that have a quantity of 0\n if slots[str] == 0 then\n slots[str] = nil\n\n -- also remove related minicard\n slots[str .. \"-m\"] = nil\n end\n end\n end\n end\n\n -- jump mark at the end of the loop\n ::nextLine::\n end\n end\n\n -- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.extractBondedCards = function(slots)\n -- Create a list of bonded cards first so we don't modify slots while iterating\n local bondedCards = { }\n local bondedList = { }\n for cardId, cardCount in pairs(slots) do\n local card = allCardsBagApi.getCardById(cardId)\n if card ~= nil and card.metadata.bonded ~= nil then\n for _, bond in ipairs(card.metadata.bonded) do\n -- 'unlimited' upper limit for cards without this data\n local maxCount = bond.maxCount or 99\n\n -- add a bonded card for each copy of the parent card (until the max value is reached)\n bondedCards[bond.id] = math.min(bond.count * cardCount, maxCount)\n\n -- We need to know which cards are bonded to determine their position, remember them\n bondedList[bond.id] = true\n\n -- Also adding taboo versions of bonded cards to the list\n bondedList[bond.id .. \"-t\"] = true\n end\n end\n end\n\n -- Add any bonded cards to the main slots list\n for bondedId, bondedCount in pairs(bondedCards) do\n slots[bondedId] = bondedCount\n end\n\n return bondedList\n end\n\n -- Check the deck for cards on its taboo list. If they're found, replace the entry in the slot with the Taboo id (i.e. \"XXXX\" becomes \"XXXX-t\")\n ---@param tabooId string The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.checkTaboos = function(tabooId, slots, playerColor)\n if tabooId then\n for cardId, _ in pairs(tabooList[tabooId].cards) do\n if slots[cardId] ~= nil then\n -- Make sure there's a taboo version of the card before we replace it\n -- SCED only maintains the most recent taboo cards. If a deck is using\n -- an older taboo list it's possible the card isn't a taboo any more\n local tabooCard = allCardsBagApi.getCardById(cardId .. \"-t\")\n if tabooCard == nil then\n local basicCard = allCardsBagApi.getCardById(cardId)\n internal.maybePrint(\"Taboo version for \" .. basicCard.data.Nickname .. \" is not available. Using standard version\", playerColor)\n else\n slots[cardId .. \"-t\"] = slots[cardId]\n slots[cardId] = nil\n end\n end\n end\n end\n end\n\n internal.maybePrint = function(message, playerColor)\n if playerColor ~= \"None\" then\n printToAll(message, playerColor)\n end\n end\n\n -- Gets the ArkhamDB config info from the configuration object.\n ---@return table: configuration data\n internal.getConfiguration = function()\n local configuration = getObjectsWithTag(\"import_configuration_provider\")[1].getTable(\"configuration\")\n printPriority = configuration.priority\n return configuration\n end\n\n internal.fixUtf16String = function(str)\n return str:gsub(\"\\\\u(%w%w%w%w)\", function(match)\n return string.char(tonumber(match, 16))\n end)\n end\n\n Request = {\n is_done = false,\n is_successful = false\n }\n\n -- Creates a new instance of a Request. Should not be directly called. Instead use Request.start() and Request.deferred().\n ---@param uri table\n ---@param configure fun(request, status)\n ---@return Request\n function Request:new(uri, configure)\n local this = {}\n\n setmetatable(this, self)\n self.__index = self\n\n if type(uri) == \"table\" then\n uri = table.concat(uri, \"/\")\n end\n\n this.uri = uri\n WebRequest.get(uri, function(status) configure(this, status) end)\n\n return this\n end\n\n -- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.\n -- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)\n ---@param uri table\n ---@param on_success fun(request, status, vararg)\n ---@param on_error fun(status)|nil\n ---@return Request\n function Request.deferred(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request:new(uri, function(request, status)\n if (status.is_done) then\n if (status.is_error) then\n request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error\n request.is_successful = false\n request.is_done = true\n else\n on_success(request, status)\n end\n end\n end)\n end\n\n -- Creates a new request. on_success should return whether the resultant data is as expected, and the processed content of the request.\n ---@param uri table\n ---@param on_success fun(status, vararg): boolean, any\n ---@param on_error nil|fun(status, vararg): string\n ---@vararg any\n ---@return Request\n function Request.start(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request.deferred(uri, function(request, status)\n local result, message = on_success(status, table.unpack(parameters))\n if not result then request.error_message = message else request.content = message end\n request.is_successful = result\n request.is_done = true\n end, on_error, table.unpack(parameters))\n end\n\n ---@param requests Request[]\n ---@param on_success fun(content: any, vararg: any)\n ---@param on_error fun(requests: Request, vararg: any)|nil\n ---@vararg any\n function Request.with_all(requests, on_success, on_error, ...)\n local parameters = table.pack(...)\n\n Wait.condition(function()\n local results = {}\n local errors = {}\n\n for _, request in ipairs(requests) do\n if request.is_successful then\n table.insert(results, request.content)\n else\n table.insert(errors, request)\n end\n end\n\n if (#errors \u003c= 0) then\n on_success(results, table.unpack(parameters))\n elseif on_error == nil then\n for _, request in ipairs(errors) do\n internal.maybePrint(table.concat({ \"[ERROR]\", request.uri, \":\", request.error_message }))\n end\n else\n on_error(requests, table.unpack(parameters))\n end\n end, function()\n for _, request in ipairs(requests) do\n if not request.is_done then return false end\n end\n return true\n end)\n end\n\n function Request:with(callback, ...)\n local arguments = table.pack(...)\n Wait.condition(function()\n if self.is_successful then\n callback(self.content, table.unpack(arguments))\n end\n end, function() return self.is_done\n end)\n end\n\n return ArkhamDb\nend\nend)\n__bundle_register(\"playercards/SpawnBag\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/PlayerCardSpawner\")\n\n-- Allows spawning of defined lists of cards which will be created from the template in the All\n-- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while\n-- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the\n-- spawned objects. Objects moved out of this area will not be cleaned up.\n--\n-- SpawnSpec: Spawning requires a spawn specification with the following structure:\n-- {\n-- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned,\n-- but each requires a separate name\n-- cards: A list of card IDs to be spawned\n-- globalPos: Where the spawned objects should be placed, in global coordinates. This should be\n-- a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 }\n-- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with\n-- globalPos, this should be a valid Vector with x, y, and z defined\n-- spread: Optional Boolean. If present and true, cards will be spawned next to each other in a\n-- spread moving to the right. globalPos will define the location of the first card, each\n-- after that will be moved a predefined distance\n-- spreadCols: Optional integer. If spread is true, specifies the maximum columns cards will be\n-- laid out in before starting a new row. If spread is true but spreadCols is not set, all\n-- cards will be in a single row (however long that may be)\n-- }\n-- See BondedBag.ttslua for an example\ndo\n local allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n\n local SpawnBag = { }\n local internal = { }\n\n -- To assist debugging, will draw a box around the recall zone when it's set up\n local SHOW_RECALL_ZONE = false\n\n -- Distance to expand the recall zone around any added object.\n local RECALL_BUFFER_X = 0.9\n local RECALL_BUFFER_Z = 0.5\n\n -- In order to mimic the behavior of the previous memory buttons we use a temporary bag when\n -- recalling objects. This bag is tiny and transparent, and will be placed at the same location as\n -- this object. Once all placed cards are recalled bag to this bag, it will be destroyed\n local RECALL_BAG = {\n Name = \"Bag\",\n Transform = {\n scaleX = 0.01,\n scaleY = 0.01,\n scaleZ = 0.01,\n },\n ColorDiffuse = {\n r = 0,\n g = 0,\n b = 0,\n a = 0,\n },\n Locked = true,\n Grid = true,\n Snap = false,\n Tooltip = false\n }\n\n -- Tracks what has been placed by this \"bag\" so they can be recalled\n local placedSpecs = { }\n local placedObjectGuids = { }\n local recallZone = nil\n\n -- Loads a table of saved state, extracted during the parent object's onLoad\n SpawnBag.loadFromSave = function(saveTable)\n placedSpecs = saveTable.placed\n placedObjectGuids = saveTable.placedObjects\n recallZone = saveTable.recall\n end\n\n -- Generates a table of save state that can be included in the parent object's onSave\n SpawnBag.getStateForSave = function()\n return {\n placed = placedSpecs,\n placedObjects = placedObjectGuids,\n recall = recallZone,\n }\n end\n\n -- Places the given spawnSpec on the table. See comment at the start of the file for spawnSpec table data and examples\n SpawnBag.spawn = function(spawnSpec)\n -- Limit to one placement at a time\n if (placedSpecs[spawnSpec.name]) then\n return\n end\n if (spawnSpec == nil) then\n -- TODO: error here\n return\n end\n local cardsToSpawn = { }\n local cardList = spawnSpec.cards\n for _, cardId in ipairs(cardList) do\n local cardData = allCardsBagApi.getCardById(cardId)\n if (cardData ~= nil) then\n table.insert(cardsToSpawn, cardData)\n else\n -- TODO: error here\n end\n end\n if (spawnSpec.spread) then\n Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject)\n else\n -- TTS decks come out in reverse order of the cards, reverse the list so the input order stays\n -- This only applies for decks; spreads are spawned by us in the order given\n if spawnSpec.rotation.z ~= 180 then\n cardsToSpawn = internal.reverseList(cardsToSpawn)\n end\n Spawner.spawnCards(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, internal.recordPlacedObject)\n end\n placedSpecs[spawnSpec.name] = true\n end\n\n -- Recalls all spawned objects to the bag, and clears the placedObjectGuids list\n ---@param fast boolean If true, cards will be deleted directly without faking the bag recall.\n SpawnBag.recall = function(fast)\n if fast then\n internal.deleteSpawned()\n else\n internal.recallSpawned()\n end\n\n -- We've recalled everything we can, some cards may have been moved out of the\n -- card area. Just reset at this point.\n placedSpecs = { }\n placedObjectGuids = { }\n recallZone = nil\n end\n\n -- Deleted all spawned cards.\n internal.deleteSpawned = function()\n for guid, _ in pairs(placedObjectGuids) do\n local obj = getObjectFromGUID(guid)\n if (obj ~= nil) then\n if (internal.isInRecallZone(obj)) then\n obj.destruct()\n end\n placedObjectGuids[guid] = nil\n end\n end\n end\n\n -- Recalls spawned cards with a fake bag that replicates the memory bag recall style.\n internal.recallSpawned = function()\n local trash = spawnObjectData({data = RECALL_BAG, position = self.getPosition()})\n for guid, _ in pairs(placedObjectGuids) do\n local obj = getObjectFromGUID(guid)\n if (obj ~= nil) then\n if (internal.isInRecallZone(obj)) then\n trash.putObject(obj)\n end\n placedObjectGuids[guid] = nil\n end\n end\n\n trash.destruct()\n end\n\n\n -- Callback for when an object has been spawned. Tracks the object for later recall and updates the\n -- recall zone.\n internal.recordPlacedObject = function(spawned)\n placedObjectGuids[spawned.getGUID()] = true\n internal.expandRecallZone(spawned)\n end\n\n -- Expands the current recall zone based on the position of the given object. The recall zone will\n -- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer\n internal.expandRecallZone = function(spawnedCard)\n local pos = spawnedCard.getPosition()\n if (recallZone == nil) then\n -- First card out of the bag, initialize surrounding that\n recallZone = { }\n recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z }\n recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z }\n return\n else\n if (pos.x \u003e recallZone.upperLeft.x) then\n recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X\n end\n if (pos.x \u003c recallZone.lowerRight.x) then\n recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X\n end\n if (pos.z \u003e recallZone.upperLeft.z) then\n recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z\n end\n if (pos.z \u003c recallZone.lowerRight.z) then\n recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z\n end\n end\n if (SHOW_RECALL_ZONE) then\n local y = 1.5\n local thick = 0.05\n Global.setVectorLines({\n {\n points = { {recallZone.upperLeft.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.lowerRight.z} },\n color = {1,0,0},\n thickness = thick,\n rotation = {0,0,0}\n },\n {\n points = { {recallZone.upperLeft.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.lowerRight.z} },\n color = {1,0,0},\n thickness = thick,\n rotation = {0,0,0}\n },\n {\n points = { {recallZone.lowerRight.x,y,recallZone.lowerRight.z}, {recallZone.lowerRight.x,y,recallZone.upperLeft.z} },\n color = {1,0,0},\n thickness = thick,\n rotation = {0,0,0}\n },\n {\n points = { {recallZone.lowerRight.x,y,recallZone.upperLeft.z}, {recallZone.upperLeft.x,y,recallZone.upperLeft.z} },\n color = {1,0,0},\n thickness = thick,\n rotation = {0,0,0}\n }\n })\n end\n end\n\n -- Checks to see if the given object is in the current recall zone. If there isn't a recall zone,\n -- will return true so that everything can be easily cleaned up.\n internal.isInRecallZone = function(obj)\n if (recallZone == nil) then\n return true\n end\n local pos = obj.getPosition()\n return (pos.x \u003c recallZone.upperLeft.x and pos.x \u003e recallZone.lowerRight.x\n and pos.z \u003c recallZone.upperLeft.z and pos.z \u003e recallZone.lowerRight.z)\n end\n\n internal.reverseList = function(list)\n local reversed = { }\n for i = 1, #list do\n reversed[i] = list[#list - i + 1]\n end\n\n return reversed\n end\n\n return SpawnBag\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/PlayerCardPanel\", function(require, _LOADED, __bundle_register, __bundle_modules)\n---@diagnostic disable: param-type-mismatch\nrequire(\"playercards/PlayerCardPanelData\")\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\nlocal arkhamDb = require(\"arkhamdb/ArkhamDb\")\nlocal spawnBag = require(\"playercards/SpawnBag\")\n\n-- Size and position information for the three rows of class buttons\nlocal CIRCLE_BUTTON_SIZE = 250\nlocal CLASS_BUTTONS_X_OFFSET = 0.1325\nlocal INVESTIGATOR_ROW_START = Vector(0.125, 0.1, -0.447)\nlocal LEVEL_ZERO_ROW_START = Vector(0.125, 0.1, -0.007)\nlocal UPGRADED_ROW_START = Vector(0.125, 0.1, 0.333)\n\n-- Size and position information for the two blocks of other buttons\nlocal MISC_BUTTONS_X_OFFSET = 0.155\nlocal WEAKNESS_ROW_START = Vector(0.157, 0.1, 0.666)\nlocal OTHER_ROW_START = Vector(0.605, 0.1, 0.666)\n\n-- Size and position information for the Cycle (box) buttons\nlocal CYCLE_BUTTON_SIZE = 468\nlocal CYCLE_BUTTON_START = Vector(-0.716, 0.1, -0.39)\nlocal CYCLE_COLUMN_COUNT = 3\nlocal CYCLE_BUTTONS_X_OFFSET = 0.267\nlocal CYCLE_BUTTONS_Z_OFFSET = 0.2665\n\nlocal STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 }\nlocal TRANSPARENT = { 0, 0, 0, 0 }\nlocal STARTER_DECK_MODE_STARTERS = \"starters\"\nlocal STARTER_DECK_MODE_CARDS_ONLY = \"cards\"\n\nlocal FACE_UP_ROTATION = { x = 0, y = 270, z = 0}\nlocal FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180}\n\n-- ---------- IMPORTANT ----------\n-- Coordinates defined below are in global dimensions relative to the panel - DO NOT USE THESE\n-- DIRECTLY. Call scalePositions() before use, and reference the variables below\n\n-- Layout width for a single card, in global coordinate space\nlocal CARD_WIDTH = 2.3\n\n-- Coordinates to begin laying out cards. These vary based on the cards that are being placed by\n-- considering the width of the cards, number of cards, and desired spread intervals.\n-- IMPORTANT! Because of the mix of global card sizes and relative-to-scale positions, the X and Y\n-- coordinates on these provide global disances while the Z is local.\nlocal START_POSITIONS = {\n classCards = Vector(CARD_WIDTH * 9.5, 2, 1.4),\n investigator = Vector(6 * 2.5, 2, 1.3),\n cycle = Vector(CARD_WIDTH * 9.5, 2, 2.4),\n other = Vector(CARD_WIDTH * 9.5, 2, 1.4),\n randomWeakness = Vector(0, 2, 1.4),\n -- Because the card spread is handled by the SpawnBag, we don't know (programatically) where this\n -- should be placed. If more customizable cards are added it will need to be moved.\n summonedServitor = Vector(CARD_WIDTH * -7.5, 2, 1.7)\n}\n\n-- Shifts to move rows of cards, and groups of rows, as different groupings are laid out\nlocal CARD_ROW_OFFSET = 3.7\nlocal CARD_GROUP_OFFSET = 2\n\n-- Position offsets for investigator decks in investigator mode, defines the spacing for how the\n-- rows and columns are laid out\nlocal INVESTIGATOR_POSITION_SHIFT_ROW = Vector(0, 0, 11)\nlocal INVESTIGATOR_POSITION_SHIFT_COL = Vector(-6, 0, 0)\nlocal INVESTIGATOR_MAX_COLS = 6\n\n-- Positions relative to the minicard to place other stacks. Both signature card piles and starter\n-- decks use SIGNATURE_OFFSET\nlocal INVESTIGATOR_CARD_OFFSET = Vector(0, 0, 2.55)\nlocal INVESTIGATOR_SIGNATURE_OFFSET = Vector(0, 0, 5.75)\n\n-- USE THESE! Positions and offset shifts accounting for the scale of the panel\nlocal startPositions\nlocal cardRowOffset\nlocal cardGroupOffset\nlocal investigatorPositionShiftRow\nlocal investigatorPositionShiftCol\nlocal investigatorCardOffset\nlocal investigatorSignatureOffset\n\nlocal CLASS_LIST = { \"Guardian\", \"Seeker\", \"Rogue\", \"Mystic\", \"Survivor\", \"Neutral\" }\nlocal CYCLE_LIST = {\n \"Core\",\n \"The Dunwich Legacy\",\n \"The Path to Carcosa\",\n \"The Forgotten Age\",\n \"The Circle Undone\",\n \"The Dream-Eaters\",\n \"The Innsmouth Conspiracy\",\n \"Edge of the Earth\",\n \"The Scarlet Keys\",\n \"The Feast of Hemlock Vale\",\n \"Investigator Packs\"\n}\n\nlocal excludedNonBasicWeaknesses\n\nlocal starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY\nlocal helpVisibleToPlayers = { }\n\nfunction onSave()\n return JSON.encode({ spawnBagState = spawnBag.getStateForSave() })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local saveState = JSON.decode(savedData) or { }\n if saveState.spawnBagState ~= nil then\n spawnBag.loadFromSave(saveState.spawnBagState)\n end\n end\n arkhamDb.initialize()\n buildExcludedWeaknessList()\n createButtons()\nend\n\n-- Build a list of non-basic weaknesses which should be excluded from the last weakness set,\n-- including all signature cards and evolved weaknesses.\nfunction buildExcludedWeaknessList()\n excludedNonBasicWeaknesses = { }\n for _, investigator in pairs(INVESTIGATORS) do\n for _, signatureId in ipairs(investigator.signatures) do\n excludedNonBasicWeaknesses[signatureId] = true\n end\n end\n for _, weaknessId in ipairs(EVOLVED_WEAKNESSES) do\n excludedNonBasicWeaknesses[weaknessId] = true\n end\nend\n\nfunction createButtons()\n createHelpButton()\n createInvestigatorButtons()\n createLevelZeroButtons()\n createUpgradedButtons()\n createWeaknessButtons()\n createOtherButtons()\n createCycleButtons()\n createClearButton()\n -- Create investigator mode buttons last so the indexes are set when we need to update them\n createInvestigatorModeButtons()\nend\n\nfunction createHelpButton()\n self.createButton({\n function_owner = self,\n click_function = \"toggleHelp\",\n position = Vector(0.845, 0.1, -0.855),\n height = 180,\n width = 180,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n })\nend\n\nfunction createInvestigatorButtons()\n local invButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = INVESTIGATOR_ROW_START:copy()\n for _, class in ipairs(CLASS_LIST) do\n invButtonParams.click_function = \"spawnInvestigators\" .. class\n invButtonParams.position = buttonPos\n self.createButton(invButtonParams)\n buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET\n self.setVar(invButtonParams.click_function, function(_, _, _) spawnInvestigatorGroup(class) end)\n end\nend\n\nfunction createLevelZeroButtons()\n local l0ButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = LEVEL_ZERO_ROW_START:copy()\n for _, class in ipairs(CLASS_LIST) do\n l0ButtonParams.click_function = \"spawnBasic\" .. class\n l0ButtonParams.position = buttonPos\n self.createButton(l0ButtonParams)\n buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET\n self.setVar(l0ButtonParams.click_function, function(_, _, _) spawnClassCards(class, false) end)\n end\nend\n\nfunction createUpgradedButtons()\n local upgradedButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = UPGRADED_ROW_START:copy()\n for _, class in ipairs(CLASS_LIST) do\n upgradedButtonParams.click_function = \"spawnUpgraded\" .. class\n upgradedButtonParams.position = buttonPos\n self.createButton(upgradedButtonParams)\n buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET\n self.setVar(upgradedButtonParams.click_function, function(_, _, _) spawnClassCards(class, true) end)\n end\nend\n\nfunction createWeaknessButtons()\n local weaknessButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = WEAKNESS_ROW_START:copy()\n weaknessButtonParams.click_function = \"spawnWeaknesses\"\n weaknessButtonParams.tooltip = \"All Weaknesses\"\n weaknessButtonParams.position = buttonPos\n self.createButton(weaknessButtonParams)\n buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET\n weaknessButtonParams.click_function = \"spawnRandomWeakness\"\n weaknessButtonParams.tooltip = \"Random Basic Weakness\"\n weaknessButtonParams.position = buttonPos\n self.createButton(weaknessButtonParams)\nend\n\nfunction createOtherButtons()\n local otherButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = OTHER_ROW_START:copy()\n otherButtonParams.click_function = \"spawnBonded\"\n otherButtonParams.tooltip = \"Bonded Cards\"\n otherButtonParams.position = buttonPos\n self.createButton(otherButtonParams)\n buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET\n otherButtonParams.click_function = \"spawnUpgradeSheets\"\n otherButtonParams.tooltip = \"Customization Upgrade Sheets\"\n otherButtonParams.position = buttonPos\n self.createButton(otherButtonParams)\nend\n\nfunction createCycleButtons()\n local cycleButtonParams = {\n function_owner = self,\n height = CYCLE_BUTTON_SIZE,\n width = CYCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = CYCLE_BUTTON_START:copy()\n local rowCount = 0\n local colCount = 0\n for _, cycle in ipairs(CYCLE_LIST) do\n cycleButtonParams.click_function = \"spawnCycle\" .. cycle\n cycleButtonParams.position = buttonPos\n cycleButtonParams.tooltip = cycle\n self.createButton(cycleButtonParams)\n self.setVar(cycleButtonParams.click_function, function(_, _, _) spawnCycle(cycle) end)\n colCount = colCount + 1\n -- If we've reached the end of a row, shift down and back to the first column\n if colCount \u003e= CYCLE_COLUMN_COUNT then\n buttonPos = CYCLE_BUTTON_START:copy()\n rowCount = rowCount + 1\n colCount = 0\n buttonPos.z = buttonPos.z + CYCLE_BUTTONS_Z_OFFSET * rowCount\n if rowCount == 3 then\n -- Account for two centered buttons on the final row\n buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET / 2\n --[[ Account for centered button on the final row\n buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET\n ]]\n end\n else\n buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET\n end\n end\nend\n\nfunction createClearButton()\n self.createButton({\n function_owner = self,\n click_function = \"deleteAll\",\n position = Vector(0, 0.1, 0.852),\n height = 170,\n width = 750,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n })\nend\n\nfunction createInvestigatorModeButtons()\n local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS\n\n self.createButton({\n function_owner = self,\n click_function = \"setCardsOnlyMode\",\n position = Vector(0.251, 0.1, -0.322),\n height = 170,\n width = 760,\n scale = Vector(0.25, 1, 0.25),\n color = starterMode and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR\n })\n self.createButton({\n function_owner = self,\n click_function = \"setStarterDeckMode\",\n position = Vector(0.66, 0.1, -0.322),\n height = 170,\n width = 760,\n scale = Vector(0.25, 1, 0.25),\n color = starterMode and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT\n })\n local checkX = starterMode and 0.52 or 0.11\n self.createButton({\n function_owner = self,\n label = \"✓\",\n click_function = \"doNothing\",\n position = Vector(checkX, 0.11, -0.317),\n height = 0,\n width = 0,\n scale = Vector(0.3, 1, 0.3),\n font_color = { 0, 0, 0 },\n color = { 1, 1, 1 }\n })\nend\n\nfunction toggleHelp(_, playerColor, _)\n if helpVisibleToPlayers[playerColor] then\n helpVisibleToPlayers[playerColor] = nil\n else\n helpVisibleToPlayers[playerColor] = true\n end\n updateHelpVisibility()\nend\n\nfunction updateHelpVisibility()\n local visibility = \"\"\n for player, _ in pairs(helpVisibleToPlayers) do\n if string.len(visibility) \u003e 0 then\n visibility = visibility .. \"|\" .. player\n else\n visibility = player\n end\n end\n self.UI.setAttribute(\"helpText\", \"visibility\", visibility)\n self.UI.setAttribute(\"helpPanel\", \"visibility\", visibility)\n self.UI.setAttribute(\"helpPanel\", \"active\", string.len(visibility) \u003e 0)\nend\n\nfunction setStarterDeckMode()\n starterDeckMode = STARTER_DECK_MODE_STARTERS\n updateStarterModeButtons()\nend\n\nfunction setCardsOnlyMode()\n starterDeckMode = STARTER_DECK_MODE_CARDS_ONLY\n updateStarterModeButtons()\nend\n\nfunction updateStarterModeButtons()\n local buttonCount = #self.getButtons()\n -- Buttons are 0-indexed, so the last three are -1, -2, and -3 from the size\n self.removeButton(buttonCount - 1)\n self.removeButton(buttonCount - 2)\n self.removeButton(buttonCount - 3)\n createInvestigatorModeButtons()\nend\n\n-- Clears the table and updates positions based on scale (should be called before ANY card placement)\nfunction prepareToPlaceCards()\n deleteAll()\n scalePositions()\nend\n\n-- Updates the positions based on the current object scale to ensure the relative layout functions\n-- properly at different scales.\nfunction scalePositions()\n -- Assume scaling is consistent in X and Z dimensions\n local scale = 1 / self.getScale().x\n startPositions = { }\n for key, pos in pairs(START_POSITIONS) do\n -- Because a scaled object means a different global size, using global distance for Z results in\n -- the cards being closer or farther depending on the scale. Leave the Z values and only scale X and Y\n startPositions[key] = Vector(pos)\n startPositions[key].x = startPositions[key].x * scale\n startPositions[key].y = startPositions[key].y * scale\n end\n cardRowOffset = CARD_ROW_OFFSET * scale\n cardGroupOffset = CARD_GROUP_OFFSET * scale\n investigatorPositionShiftRow = Vector(INVESTIGATOR_POSITION_SHIFT_ROW):scale(scale)\n investigatorPositionShiftCol = Vector(INVESTIGATOR_POSITION_SHIFT_COL):scale(scale)\n investigatorCardOffset = Vector(INVESTIGATOR_CARD_OFFSET):scale(scale)\n investigatorSignatureOffset = Vector(INVESTIGATOR_SIGNATURE_OFFSET):scale(scale)\nend\n\n-- Deletes all cards currently placed on the table\nfunction deleteAll()\n spawnBag.recall(true)\nend\n\n-- Spawn an investigator group, based on the current UI setting for either investigators or starter\n-- decks.\n---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData\nfunction spawnInvestigatorGroup(groupName)\n local starterMode = starterDeckMode == STARTER_DECK_MODE_STARTERS\n prepareToPlaceCards()\n Wait.frames(function()\n if starterMode then\n spawnStarters(groupName)\n else\n spawnInvestigators(groupName)\n end\n end, 2)\nend\n\n-- Spawn cards for all investigators in the given group. This creates piles for all defined\n-- investigator cards and minicards as well as the signature cards.\n---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData\nfunction spawnInvestigators(groupName)\n if INVESTIGATOR_GROUPS[groupName] == nil then\n printToAll(\"No \" .. groupName .. \" data yet\")\n return\n end\n\n local col = 1\n local row = 1\n local investigatorCount = #INVESTIGATOR_GROUPS[groupName]\n local position = getInvestigatorRowStartPos(investigatorCount, row)\n\n for i, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do\n for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec(investigatorName, INVESTIGATORS[investigatorName], position)) do\n spawnBag.spawn(spawnSpec)\n end\n position:add(investigatorPositionShiftCol)\n col = col + 1\n if col \u003e INVESTIGATOR_MAX_COLS then\n col = 1\n row = row + 1\n position = getInvestigatorRowStartPos(investigatorCount, row)\n end\n end\nend\n\nfunction getInvestigatorRowStartPos(investigatorCount, row)\n local rowStart = Vector(startPositions.investigator)\n rowStart:add(Vector(\n investigatorPositionShiftRow.x * (row - 1),\n investigatorPositionShiftRow.y * (row - 1),\n investigatorPositionShiftRow.z * (row - 1)))\n local investigatorsInRow = math.min(investigatorCount - INVESTIGATOR_MAX_COLS * (row - 1), INVESTIGATOR_MAX_COLS)\n rowStart:add(Vector(\n investigatorPositionShiftCol.x * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,\n investigatorPositionShiftCol.y * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,\n investigatorPositionShiftCol.z * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2))\n return rowStart\nend\n\n-- Creates the spawn spec for the investigator's signature cards.\n---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData\n---@param investigatorData table Spawn definition for the investigator, retrieved from INVESTIGATORS\n---@param position tts__Vector Where to spawn the minicard; investigagor cards will be placed below\nfunction buildInvestigatorSpawnSpec(investigatorName, investigatorData, position)\n local sigPos = Vector(position):add(investigatorSignatureOffset)\n local spawns = buildCommonSpawnSpec(investigatorName, investigatorData, position)\n table.insert(spawns, {\n name = investigatorName .. \"signatures\",\n cards = investigatorData.signatures,\n globalPos = self.positionToWorld(sigPos),\n rotation = FACE_UP_ROTATION\n })\n\n return spawns\nend\n\n-- Builds the spawn specs for minicards and investigator cards. These are common enough to be\n-- shared, and will only differ in whether they spawn the full stack of possible investigator and\n-- minicards, or only the first of each.\n---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData\n---@param investigatorData table Spawn definition for the investigator, retrieved from INVESTIGATORS\n---@param position tts__Vector Where to spawn the minicard; investigagor cards will be placed below\n---@param oneCardOnly? boolean If true, will spawn only the first card in the investigator card\n--- and minicard lists. Otherwise, spawn them all in a deck\nfunction buildCommonSpawnSpec(investigatorName, investigatorData, position, oneCardOnly)\n local cardPos = Vector(position):add(investigatorCardOffset)\n return {\n {\n name = investigatorName .. \"minicards\",\n cards = oneCardOnly and { investigatorData.minicards[1] } or investigatorData.minicards,\n globalPos = self.positionToWorld(position),\n rotation = FACE_UP_ROTATION\n },\n {\n name = investigatorName .. \"cards\",\n cards = oneCardOnly and { investigatorData.cards[1] } or investigatorData.cards,\n globalPos = self.positionToWorld(cardPos),\n rotation = FACE_UP_ROTATION\n }\n }\nend\n\n-- Spawns all starter decks (single minicard and investigator card, plus the starter deck) for\n-- investigators in the given group.\n---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData\nfunction spawnStarters(groupName)\n local col = 1\n local row = 1\n local investigatorCount = #INVESTIGATOR_GROUPS[groupName]\n local position = getInvestigatorRowStartPos(investigatorCount, row)\n for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do\n spawnStarterDeck(investigatorName, INVESTIGATORS[investigatorName], position)\n position:add(investigatorPositionShiftCol)\n col = col + 1\n if col \u003e INVESTIGATOR_MAX_COLS then\n col = 1\n row = row + 1\n position = getInvestigatorRowStartPos(investigatorCount, row)\n end\n end\nend\n\n-- Spawns the defined starter deck for the given investigator's.\n---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData\nfunction spawnStarterDeck(investigatorName, investigatorData, position)\n for _, spawnSpec in ipairs(buildCommonSpawnSpec(investigatorName, investigatorData, position, true)) do\n spawnBag.spawn(spawnSpec)\n end\n local deckPos = Vector(position):add(investigatorSignatureOffset)\n arkhamDb.getDecklist(\"None\", investigatorData.starterDeck, true, false, false, function(slots)\n local cardIdList = { }\n for id, count in pairs(slots) do\n for i = 1, count do\n table.insert(cardIdList, id)\n end\n end\n spawnBag.spawn({\n name = investigatorName..\"starter\",\n cards = cardIdList,\n globalPos = self.positionToWorld(deckPos),\n rotation = FACE_DOWN_ROTATION\n })\n end)\nend\n-- Clears the currently placed cards, then places cards for the given class and level spread\n---@param cardClass string Class to place (\"Guardian\", \"Seeker\", etc)\n---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.\nfunction spawnClassCards(cardClass, isUpgraded)\n prepareToPlaceCards()\n Wait.frames(function() placeClassCards(cardClass, isUpgraded) end, 2)\nend\n\n-- Spawn the class cards.\n---@param cardClass string Class to place (\"Guardian\", \"Seeker\", etc)\n---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.\nfunction placeClassCards(cardClass, isUpgraded)\n local indexReady = allCardsBagApi.isIndexReady()\n if (not indexReady) then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", {0.9, 0.2, 0.2})\n return\n end\n local cardIdList = allCardsBagApi.getCardsByClassAndLevel(cardClass, isUpgraded)\n\n local skillList = { }\n local eventList = { }\n local assetList = { }\n for _, cardId in ipairs(cardIdList) do\n local cardMetadata = allCardsBagApi.getCardById(cardId).metadata\n if (cardMetadata.type == \"Skill\") then\n table.insert(skillList, cardId)\n elseif (cardMetadata.type == \"Event\") then\n table.insert(eventList, cardId)\n elseif (cardMetadata.type == \"Asset\") then\n table.insert(assetList, cardId)\n end\n end\n local groupPos = Vector(startPositions.classCards)\n if #skillList \u003e 0 then\n spawnBag.spawn({\n name = cardClass .. (isUpgraded and \"upgraded\" or \"basic\"),\n cards = skillList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#skillList / 20) * cardRowOffset + cardGroupOffset\n end\n if #eventList \u003e 0 then\n spawnBag.spawn({\n name = cardClass .. \"event\" .. (isUpgraded and \"upgraded\" or \"basic\"),\n cards = eventList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#eventList / 20) * cardRowOffset + cardGroupOffset\n end\n if #assetList \u003e 0 then\n spawnBag.spawn({\n name = cardClass .. \"asset\" .. (isUpgraded and \"upgraded\" or \"basic\"),\n cards = assetList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n end\nend\n\n-- Spawns the investigator sets and all cards for the given cycle\n---@param cycle string Name of a cycle, should match the standard used in card metadata\nfunction spawnCycle(cycle)\n prepareToPlaceCards()\n spawnInvestigators(cycle)\n local indexReady = allCardsBagApi.isIndexReady()\n if (not indexReady) then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", {0.9, 0.2, 0.2})\n return\n end\n local cycleCardList = allCardsBagApi.getCardsByCycle(cycle)\n local copiedList = { }\n for i, id in ipairs(cycleCardList) do\n copiedList[i] = id\n end\n spawnBag.spawn({\n name = \"cycle\"..cycle,\n cards = copiedList,\n globalPos = self.positionToWorld(startPositions.cycle),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\nend\n\nfunction spawnBonded()\n prepareToPlaceCards()\n spawnBag.spawn({\n name = \"bonded\",\n cards = BONDED_CARD_LIST,\n globalPos = self.positionToWorld(startPositions.classCards),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\nend\n\nfunction spawnUpgradeSheets()\n prepareToPlaceCards()\n spawnBag.spawn({\n name = \"upgradeSheets\",\n cards = UPGRADE_SHEET_LIST,\n globalPos = self.positionToWorld(startPositions.classCards),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n spawnBag.spawn({\n name = \"servitor\",\n cards = { \"09080-m\" },\n globalPos = self.positionToWorld(startPositions.summonedServitor),\n rotation = FACE_UP_ROTATION,\n })\nend\n\n-- Clears the current cards, and places all basic weaknesses on the table.\nfunction spawnWeaknesses()\n prepareToPlaceCards()\n local indexReady = allCardsBagApi.isIndexReady()\n if (not indexReady) then\n broadcastToAll(\"Still loading player cards, please try again in a few seconds\", {0.9, 0.2, 0.2})\n return\n end\n local weaknessIdList = allCardsBagApi.getUniqueWeaknesses()\n local basicWeaknessList = { }\n local otherWeaknessList = { }\n for i, id in ipairs(weaknessIdList) do\n local cardMetadata = allCardsBagApi.getCardById(id).metadata\n if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount \u003e 0 then\n table.insert(basicWeaknessList, id)\n elseif excludedNonBasicWeaknesses[id] == nil then\n table.insert(otherWeaknessList, id)\n end\n end\n local groupPos = Vector(startPositions.classCards)\n spawnBag.spawn({\n name = \"basicWeaknesses\",\n cards = basicWeaknessList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#basicWeaknessList / 20) * cardRowOffset + cardGroupOffset\n spawnBag.spawn({\n name = \"evolvedWeaknesses\",\n cards = EVOLVED_WEAKNESSES,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#EVOLVED_WEAKNESSES / 20) * cardRowOffset + cardGroupOffset\n spawnBag.spawn({\n name = \"otherWeaknesses\",\n cards = otherWeaknessList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\nend\n\nfunction spawnRandomWeakness()\n prepareToPlaceCards()\n local weaknessId = allCardsBagApi.getRandomWeaknessId()\n if (weaknessId == nil) then\n broadcastToAll(\"All basic weaknesses are in play!\", {0.9, 0.2, 0.2})\n return\n end\n spawnBag.spawn({\n name = \"randomWeakness\",\n cards = { weaknessId },\n globalPos = self.positionToWorld(startPositions.randomWeakness),\n rotation = FACE_UP_ROTATION,\n })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"arkhamdb/ArkhamDb\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n local playAreaApi = require(\"core/PlayAreaApi\")\n\n local ArkhamDb = {}\n local internal = {}\n \n local tabooList = {}\n local configuration\n\n local RANDOM_WEAKNESS_ID = \"01000\"\n\n ---@class Request\n local Request = {}\n\n -- Sets up the ArkhamDb interface. Should be called from the parent object on load.\n ArkhamDb.initialize = function()\n configuration = internal.getConfiguration()\n Request.start({ configuration.api_uri, configuration.taboo }, function(status)\n local json = JSON.decode(internal.fixUtf16String(status.text))\n for _, taboo in pairs(json) do\n local cards = {}\n\n for _, card in pairs(JSON.decode(taboo.cards)) do\n cards[card.code] = true\n end\n\n tabooList[taboo.id] = {\n date = taboo.date_start,\n cards = cards\n }\n end\n return true, nil\n end)\n end\n\n -- Start the deck build process for the given player color and deck ID. This\n -- will retrieve the deck from ArkhamDB, and pass to a callback for processing.\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\").\n ---@param deckId string ArkhamDB deck id to be loaded\n ---@param isPrivate boolean Whether this deck is published or private on ArkhamDB\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n ---@return boolean\n ---@return string\n ArkhamDb.getDecklist = function(\n playerColor,\n deckId,\n isPrivate,\n loadNewest,\n loadInvestigators,\n callback)\n -- Get a simple card to see if the bag indexes are complete. If not, abort\n -- the deck load. The called method will handle player notification.\n local checkCard = allCardsBagApi.getCardById(\"01001\")\n if (checkCard ~= nil and checkCard.data == nil) then\n return false, \"Indexing not complete\"\n end\n\n local deckUri = {\n configuration.api_uri,\n isPrivate and configuration.private_deck or configuration.public_deck,\n deckId\n }\n\n local deck = Request.start(deckUri, function(status)\n if string.find(status.text, \"\u003c!DOCTYPE html\u003e\") then\n internal.maybePrint(\"Private deck ID \" .. deckId .. \" is not shared.\", playerColor)\n return false, \"Private deck \" .. deckId .. \" is not shared\"\n end\n\n local json = JSON.decode(internal.fixUtf16String(status.text))\n\n if not json then\n internal.maybePrint(\"Deck ID \" .. deckId .. \" not found.\", playerColor)\n return false, \"Deck not found!\"\n end\n\n return true, json\n end)\n\n deck:with(internal.onDeckResult, playerColor, loadNewest, loadInvestigators, callback)\n end\n\n -- Logs that a card could not be loaded in the mod by printing it to the console in the given\n -- color of the player owning the deck. Attempts to look up the name on ArkhamDB for clarity,\n -- but prints the card ID if the name cannot be retrieved.\n ---@param cardId string ArkhamDB ID of the card that could not be found\n ---@param playerColor string Color of the player's deck that had the problem\n ArkhamDb.logCardNotFound = function(cardId, playerColor)\n local request = Request.start({\n configuration.api_uri,\n configuration.cards,\n cardId\n },\n function(result)\n local adbCardInfo = JSON.decode(internal.fixUtf16String(result.text))\n local cardName = adbCardInfo.real_name\n if (cardName ~= nil) then\n if (adbCardInfo.xp ~= nil and adbCardInfo.xp \u003e 0) then\n cardName = cardName .. \" (\" .. adbCardInfo.xp .. \")\"\n end\n internal.maybePrint(\"Card not found: \" .. cardName .. \", card ID \" .. cardId, playerColor)\n else\n internal.maybePrint(\"Card not found in ArkhamDB/Index, ID \" .. cardId, playerColor)\n end\n end)\n end\n\n -- Callback when the deck information is received from ArkhamDB. Parses the\n -- response then applies standard transformations to the deck such as adding\n -- random weaknesses and checking for taboos. Once the deck is processed,\n -- passes to loadCards to actually spawn the defined deck.\n ---@param deck table ArkhamImportDeck\n ---@param playerColor string Color name of the player mat to place this deck on (e.g. \"Red\")\n ---@param loadNewest boolean Whether the newest version of this deck should be loaded\n ---@param loadInvestigators boolean Whether investigator cards should be loaded as part of this deck\n ---@param callback function Callback which will be sent the results of this load.\n --- Parameters to the callback will be:\n --- slots table A map of card ID to count in the deck\n --- investigatorCode String. ID of the investigator in this deck\n --- bondedList A table of cardID keys to meaningless values. Card IDs in this list were\n --- added from a parent bonded card.\n --- customizations table The decoded table of customization upgrades in this deck\n --- playerColor String. Color this deck is being loaded for\n internal.onDeckResult = function(deck, playerColor, loadNewest, loadInvestigators, callback)\n -- Load the next deck in the upgrade path if the option is enabled\n if (loadNewest and deck.next_deck ~= nil and deck.next_deck ~= \"\") then\n buildDeck(playerColor, deck.next_deck)\n return\n end\n\n internal.maybePrint(table.concat({ \"Found decklist: \", deck.name }), playerColor)\n\n -- Initialize deck slot table and perform common transformations. The order of these should not\n -- be changed, as later steps may act on cards added in each. For example, a random weakness or\n -- investigator may have bonded cards or taboo entries, and should be present\n local slots = deck.slots\n internal.maybeDrawRandomWeakness(slots, playerColor)\n\n -- handles alternative investigators (parallel, promo or revised art)\n local loadAltInvestigator = \"normal\"\n if loadInvestigators then\n loadAltInvestigator = internal.addInvestigatorCards(deck, slots)\n end\n\n internal.maybeModifyDeckFromDescription(slots, deck.description_md, playerColor)\n internal.maybeAddSummonedServitor(slots)\n internal.maybeAddOnTheMend(slots, playerColor)\n internal.maybeAddRealityAcidReference(slots)\n local bondList = internal.extractBondedCards(slots)\n internal.checkTaboos(deck.taboo_id, slots, playerColor)\n internal.maybeAddUpgradeSheets(slots)\n\n -- get upgrades for customizable cards\n local customizations = {}\n if deck.meta then\n customizations = JSON.decode(deck.meta)\n end\n\n callback(slots, deck.investigator_code, bondList, customizations, playerColor, loadAltInvestigator)\n end\n\n -- Checks to see if the slot list includes the random weakness ID. If it does,\n -- removes it from the deck and replaces it with the ID of a random basic weakness provided by the\n -- all cards bag\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n --- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast\n --- if a weakness is added.\n internal.maybeDrawRandomWeakness = function(slots, playerColor)\n local randomWeaknessAmount = slots[RANDOM_WEAKNESS_ID] or 0\n slots[RANDOM_WEAKNESS_ID] = nil\n\n if randomWeaknessAmount \u003e 0 then\n for i = 1, randomWeaknessAmount do\n local weaknessId = allCardsBagApi.getRandomWeaknessId()\n slots[weaknessId] = (slots[weaknessId] or 0) + 1\n end\n internal.maybePrint(\"Added \" .. randomWeaknessAmount .. \" random basic weakness(es) to deck\", playerColor)\n end\n end\n\n -- Adds both the investigator (XXXXX) and minicard (XXXXX-m) slots with one copy each\n ---@param deck table The processed ArkhamDB deck response\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the\n --- number of those cards which will be spawned\n ---@return string: Contains the name of the art that should be loaded (\"normal\", \"promo\" or \"revised\")\n internal.addInvestigatorCards = function(deck, slots)\n local investigatorId = deck.investigator_code\n slots[investigatorId .. \"-m\"] = 1\n local deckMeta = JSON.decode(deck.meta)\n\n -- handling alternative investigator art and parallel investigators\n local loadAltInvestigator = \"normal\"\n if deckMeta ~= nil then\n local altFrontId = tonumber(deckMeta.alternate_front) or 0\n local altBackId = tonumber(deckMeta.alternate_back) or 0\n local altArt = { front = \"normal\", back = \"normal\" }\n\n -- translating front ID\n if altFrontId \u003e 90000 and altFrontId \u003c 90100 then\n altArt.front = \"parallel\"\n elseif altFrontId \u003e 01500 and altFrontId \u003c 01506 then\n altArt.front = \"revised\"\n elseif altFrontId \u003e 98000 then\n altArt.front = \"promo\"\n end\n\n -- translating back ID\n if altBackId \u003e 90000 and altBackId \u003c 90100 then\n altArt.back = \"parallel\"\n elseif altBackId \u003e 01500 and altBackId \u003c 01506 then\n altArt.back = \"revised\"\n elseif altBackId \u003e 98000 then\n altArt.back = \"promo\"\n end\n\n -- updating investigatorID based on alt investigator selection\n -- precedence: parallel \u003e promo \u003e revised\n if altArt.front == \"parallel\" then\n if altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-p\"\n else\n investigatorId = investigatorId .. \"-pf\"\n end\n elseif altArt.back == \"parallel\" then\n investigatorId = investigatorId .. \"-pb\"\n elseif altArt.front == \"promo\" or altArt.back == \"promo\" then\n loadAltInvestigator = \"promo\"\n elseif altArt.front == \"revised\" or altArt.back == \"revised\" then\n loadAltInvestigator = \"revised\"\n end\n end\n slots[investigatorId] = 1\n deck.investigator_code = investigatorId\n return loadAltInvestigator\n end\n\n -- Process the card list looking for the customizable cards, and add their upgrade sheets if needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddUpgradeSheets = function(slots)\n for cardId, _ in pairs(slots) do\n -- upgrade sheets for customizable cards\n local upgradesheet = allCardsBagApi.getCardById(cardId .. \"-c\")\n if upgradesheet ~= nil then\n slots[cardId .. \"-c\"] = 1\n end\n end\n end\n\n -- Process the card list looking for the Summoned Servitor, and add its minicard to the list if\n -- needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddSummonedServitor = function(slots)\n if slots[\"09080\"] ~= nil then\n slots[\"09080-m\"] = 1\n end\n end\n\n -- On the Mend should have 1-per-investigator copies set aside, but ArkhamDB always sends 1. Update\n -- the count based on the investigator count\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n ---@param playerColor string Color of the player this deck is being loaded for. Used for broadcast if an error occurs\n internal.maybeAddOnTheMend = function(slots, playerColor)\n if slots[\"09006\"] ~= nil then\n local investigatorCount = playAreaApi.getInvestigatorCount()\n if investigatorCount ~= nil then\n slots[\"09006\"] = investigatorCount\n else\n internal.maybePrint(\"Something went wrong with the load, adding 4 copies of On the Mend\", playerColor)\n slots[\"09006\"] = 4\n end\n end\n end\n\n -- Process the card list looking for Reality Acid and adds the reference sheet when needed\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n -- of those cards which will be spawned\n internal.maybeAddRealityAcidReference = function(slots)\n if slots[\"89004\"] ~= nil then\n slots[\"89005\"] = 1\n end\n end\n\n -- Processes the deck description from ArkhamDB and modifies the slot list accordingly\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number\n ---@param description string The deck desription from ArkhamDB\n internal.maybeModifyDeckFromDescription = function(slots, description, playerColor)\n -- check for import instructions\n local pos = string.find(description, \"++SCED import instructions++\")\n if not pos then return end\n\n -- remove everything before instructions\n local tempStr = string.sub(description, pos)\n\n -- parse each line in instructions\n for line in tempStr:gmatch(\"([^\\n]+)\") do\n -- remove dashes at the start\n line = line:gsub(\"%- \", \"\")\n\n -- remove spaces\n line = line:gsub(\"%s\", \"\")\n\n -- remove balanced brackets\n line = line:gsub(\"%b()\", \"\")\n line = line:gsub(\"%b[]\", \"\")\n\n -- get instructor\n local instructor = \"\"\n for word in line:gmatch(\"%a+:\") do\n instructor = word\n break\n end\n\n -- go to the next line if no valid instructor found\n if instructor ~= \"add:\" and instructor ~= \"remove:\" then\n goto nextLine\n end\n\n -- remove instructor from line\n line = line:gsub(instructor, \"\")\n\n -- evaluate instructions\n for str in line:gmatch(\"([^,]+)\") do\n if instructor == \"add:\" then\n slots[str] = (slots[str] or 0) + 1\n elseif instructor == \"remove:\" then\n if slots[str] == nil then\n internal.maybePrint(\"Tried to remove card ID \" .. str .. \", but didn't find card in deck.\", playerColor)\n else\n slots[str] = math.max(slots[str] - 1, 0)\n\n -- fully remove cards that have a quantity of 0\n if slots[str] == 0 then\n slots[str] = nil\n\n -- also remove related minicard\n slots[str .. \"-m\"] = nil\n end\n end\n end\n end\n\n -- jump mark at the end of the loop\n ::nextLine::\n end\n end\n\n -- Process the slot list and looks for any cards which are bonded to those in the deck. Adds those cards to the slot list.\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.extractBondedCards = function(slots)\n -- Create a list of bonded cards first so we don't modify slots while iterating\n local bondedCards = { }\n local bondedList = { }\n for cardId, cardCount in pairs(slots) do\n local card = allCardsBagApi.getCardById(cardId)\n if card ~= nil and card.metadata.bonded ~= nil then\n for _, bond in ipairs(card.metadata.bonded) do\n -- 'unlimited' upper limit for cards without this data\n local maxCount = bond.maxCount or 99\n\n -- add a bonded card for each copy of the parent card (until the max value is reached)\n bondedCards[bond.id] = math.min(bond.count * cardCount, maxCount)\n\n -- We need to know which cards are bonded to determine their position, remember them\n bondedList[bond.id] = true\n\n -- Also adding taboo versions of bonded cards to the list\n bondedList[bond.id .. \"-t\"] = true\n end\n end\n end\n\n -- Add any bonded cards to the main slots list\n for bondedId, bondedCount in pairs(bondedCards) do\n slots[bondedId] = bondedCount\n end\n\n return bondedList\n end\n\n -- Check the deck for cards on its taboo list. If they're found, replace the entry in the slot with the Taboo id (i.e. \"XXXX\" becomes \"XXXX-t\")\n ---@param tabooId string The deck's taboo ID, taken from the deck response taboo_id field. May be nil, indicating that no taboo list should be used\n ---@param slots table The slot list for cards in this deck. Table key is the cardId, value is the number of those cards which will be spawned\n internal.checkTaboos = function(tabooId, slots, playerColor)\n if tabooId then\n for cardId, _ in pairs(tabooList[tabooId].cards) do\n if slots[cardId] ~= nil then\n -- Make sure there's a taboo version of the card before we replace it\n -- SCED only maintains the most recent taboo cards. If a deck is using\n -- an older taboo list it's possible the card isn't a taboo any more\n local tabooCard = allCardsBagApi.getCardById(cardId .. \"-t\")\n if tabooCard == nil then\n local basicCard = allCardsBagApi.getCardById(cardId)\n internal.maybePrint(\"Taboo version for \" .. basicCard.data.Nickname .. \" is not available. Using standard version\", playerColor)\n else\n slots[cardId .. \"-t\"] = slots[cardId]\n slots[cardId] = nil\n end\n end\n end\n end\n end\n\n internal.maybePrint = function(message, playerColor)\n if playerColor ~= \"None\" then\n printToAll(message, playerColor)\n end\n end\n\n -- Gets the ArkhamDB config info from the configuration object.\n ---@return table: configuration data\n internal.getConfiguration = function()\n local configuration = getObjectsWithTag(\"import_configuration_provider\")[1].getTable(\"configuration\")\n printPriority = configuration.priority\n return configuration\n end\n\n internal.fixUtf16String = function(str)\n return str:gsub(\"\\\\u(%w%w%w%w)\", function(match)\n return string.char(tonumber(match, 16))\n end)\n end\n\n Request = {\n is_done = false,\n is_successful = false\n }\n\n -- Creates a new instance of a Request. Should not be directly called. Instead use Request.start() and Request.deferred().\n ---@param uri table\n ---@param configure fun(request, status)\n ---@return Request\n function Request:new(uri, configure)\n local this = {}\n\n setmetatable(this, self)\n self.__index = self\n\n if type(uri) == \"table\" then\n uri = table.concat(uri, \"/\")\n end\n\n this.uri = uri\n WebRequest.get(uri, function(status) configure(this, status) end)\n\n return this\n end\n\n -- Creates a new request. on_success should set the request's is_done, is_successful, and content variables.\n -- Deferred should be used when you don't want to set is_done immediately (such as if you want to wait for another request to finish)\n ---@param uri table\n ---@param on_success fun(request, status, vararg)\n ---@param on_error fun(status)|nil\n ---@return Request\n function Request.deferred(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request:new(uri, function(request, status)\n if (status.is_done) then\n if (status.is_error) then\n request.error_message = on_error and on_error(status, table.unpack(parameters)) or status.error\n request.is_successful = false\n request.is_done = true\n else\n on_success(request, status)\n end\n end\n end)\n end\n\n -- Creates a new request. on_success should return whether the resultant data is as expected, and the processed content of the request.\n ---@param uri table\n ---@param on_success fun(status, vararg): boolean, any\n ---@param on_error nil|fun(status, vararg): string\n ---@return Request\n function Request.start(uri, on_success, on_error, ...)\n local parameters = table.pack(...)\n return Request.deferred(uri, function(request, status)\n local result, message = on_success(status, table.unpack(parameters))\n if not result then request.error_message = message else request.content = message end\n request.is_successful = result\n request.is_done = true\n end, on_error, table.unpack(parameters))\n end\n\n ---@param requests Request[]\n ---@param on_success fun(content: any, vararg: any)\n ---@param on_error fun(requests: Request, vararg: any)|nil\n function Request.with_all(requests, on_success, on_error, ...)\n local parameters = table.pack(...)\n\n Wait.condition(function()\n local results = {}\n local errors = {}\n\n for _, request in ipairs(requests) do\n if request.is_successful then\n table.insert(results, request.content)\n else\n table.insert(errors, request)\n end\n end\n\n if (#errors \u003c= 0) then\n on_success(results, table.unpack(parameters))\n elseif on_error == nil then\n for _, request in ipairs(errors) do\n internal.maybePrint(table.concat({ \"[ERROR]\", request.uri, \":\", request.error_message }))\n end\n else\n on_error(requests, table.unpack(parameters))\n end\n end, function()\n for _, request in ipairs(requests) do\n if not request.is_done then return false end\n end\n return true\n end)\n end\n\n function Request:with(callback, ...)\n local arguments = table.pack(...)\n Wait.condition(function()\n if self.is_successful then\n callback(self.content, table.unpack(arguments))\n end\n end, function() return self.is_done\n end)\n end\n\n return ArkhamDb\nend\nend)\n__bundle_register(\"playercards/PlayerCardPanel\", function(require, _LOADED, __bundle_register, __bundle_modules)\n---@diagnostic disable: param-type-mismatch\nrequire(\"playercards/PlayerCardPanelData\")\n\nlocal allCardsBagApi = require(\"playercards/AllCardsBagApi\")\nlocal arkhamDb = require(\"arkhamdb/ArkhamDb\")\nlocal spawnBag = require(\"playercards/SpawnBag\")\n\nlocal lastWeaknessTrait = \"Madness\"\n\n-- Size and position information for the three rows of class buttons\nlocal CIRCLE_BUTTON_SIZE = 250\nlocal CLASS_BUTTONS_X_OFFSET = 0.1325\nlocal INVESTIGATOR_ROW_START = Vector(0.125, 0.1, -0.447)\nlocal LEVEL_ZERO_ROW_START = Vector(0.125, 0.1, -0.007)\nlocal UPGRADED_ROW_START = Vector(0.125, 0.1, 0.333)\n\n-- Size and position information for the two blocks of other buttons\nlocal MISC_BUTTONS_X_OFFSET = 0.155\nlocal WEAKNESS_ROW_START = Vector(0.157, 0.1, 0.666)\nlocal OTHER_ROW_START = Vector(0.605, 0.1, 0.666)\n\n-- Size and position information for the Cycle (box) buttons\nlocal CYCLE_BUTTON_SIZE = 468\nlocal CYCLE_BUTTON_START = Vector(-0.716, 0.1, -0.39)\nlocal CYCLE_COLUMN_COUNT = 3\nlocal CYCLE_BUTTONS_X_OFFSET = 0.267\nlocal CYCLE_BUTTONS_Z_OFFSET = 0.2665\n\nlocal STARTER_DECK_MODE_SELECTED_COLOR = { 0.2, 0.2, 0.2, 0.8 }\nlocal TRANSPARENT = { 0, 0, 0, 0 }\n\nlocal FACE_UP_ROTATION = { x = 0, y = 270, z = 0 }\nlocal FACE_DOWN_ROTATION = { x = 0, y = 270, z = 180 }\n\n-- ---------- IMPORTANT ----------\n-- Coordinates defined below are in global dimensions relative to the panel - DO NOT USE THESE\n-- DIRECTLY. Call scalePositions() before use, and reference the variables below\n\n-- Layout width for a single card, in global coordinate space\nlocal CARD_WIDTH = 2.3\n\n-- Coordinates to begin laying out cards. These vary based on the cards that are being placed by\n-- considering the width of the cards, number of cards, and desired spread intervals.\n-- IMPORTANT! Because of the mix of global card sizes and relative-to-scale positions, the X and Y\n-- coordinates on these provide global disances while the Z is local.\nlocal START_POSITIONS = {\n classCards = Vector(CARD_WIDTH * 9.5, 2, 1.4),\n investigator = Vector(6 * 2.5, 2, 1.3),\n cycle = Vector(CARD_WIDTH * 9.5, 2, 2.4),\n other = Vector(CARD_WIDTH * 9.5, 2, 1.4),\n randomWeakness = Vector(0, 2, 1.4),\n -- Because the card spread is handled by the SpawnBag, we don't know (programatically) where this\n -- should be placed. If more customizable cards are added it will need to be moved.\n summonedServitor = Vector(CARD_WIDTH * -7.5, 2, 1.7)\n}\n\n-- Shifts to move rows of cards, and groups of rows, as different groupings are laid out\nlocal CARD_ROW_OFFSET = 3.7\nlocal CARD_GROUP_OFFSET = 2\n\n-- Position offsets for investigator decks in investigator mode, defines the spacing for how the\n-- rows and columns are laid out\nlocal INVESTIGATOR_POSITION_SHIFT_ROW = Vector(0, 0, 11)\nlocal INVESTIGATOR_POSITION_SHIFT_COL = Vector(-6, 0, 0)\nlocal INVESTIGATOR_MAX_COLS = 6\n\n-- Positions relative to the minicard to place other stacks. Both signature card piles and starter\n-- decks use SIGNATURE_OFFSET\nlocal INVESTIGATOR_CARD_OFFSET = Vector(0, 0, 2.55)\nlocal INVESTIGATOR_SIGNATURE_OFFSET = Vector(0, 0, 5.75)\n\n-- USE THESE! Positions and offset shifts accounting for the scale of the panel\nlocal startPositions\nlocal cardRowOffset\nlocal cardGroupOffset\nlocal investigatorPositionShiftRow\nlocal investigatorPositionShiftCol\nlocal investigatorCardOffset\nlocal investigatorSignatureOffset\n\nlocal CLASS_LIST = {\n \"Guardian\",\n \"Seeker\",\n \"Rogue\",\n \"Mystic\",\n \"Survivor\",\n \"Neutral\"\n}\nlocal CYCLE_LIST = {\n \"Core\",\n \"The Dunwich Legacy\",\n \"The Path to Carcosa\",\n \"The Forgotten Age\",\n \"The Circle Undone\",\n \"The Dream-Eaters\",\n \"The Innsmouth Conspiracy\",\n \"Edge of the Earth\",\n \"The Scarlet Keys\",\n \"The Feast of Hemlock Vale\",\n \"Investigator Packs\"\n}\n\nlocal excludedNonBasicWeaknesses\nlocal spawnStarterDecks = false\nlocal helpVisibleToPlayers = {}\n\nfunction onSave()\n return JSON.encode({ spawnBagState = spawnBag.getStateForSave() })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local saveState = JSON.decode(savedData) or {}\n if saveState.spawnBagState ~= nil then\n spawnBag.loadFromSave(saveState.spawnBagState)\n end\n end\n arkhamDb.initialize()\n buildExcludedWeaknessList()\n createButtons()\nend\n\n-- Build a list of non-basic weaknesses which should be excluded from the last weakness set,\n-- including all signature cards and evolved weaknesses.\nfunction buildExcludedWeaknessList()\n excludedNonBasicWeaknesses = {}\n for _, investigator in pairs(INVESTIGATORS) do\n for _, signatureId in ipairs(investigator.signatures) do\n excludedNonBasicWeaknesses[signatureId] = true\n end\n end\n for _, weaknessId in ipairs(EVOLVED_WEAKNESSES) do\n excludedNonBasicWeaknesses[weaknessId] = true\n end\nend\n\nfunction createButtons()\n createHelpButton()\n createInvestigatorButtons()\n createLevelZeroButtons()\n createUpgradedButtons()\n createWeaknessButtons()\n createOtherButtons()\n createCycleButtons()\n createClearButton()\n -- Create investigator mode buttons last so the indexes are set when we need to update them\n createInvestigatorModeButtons()\nend\n\nfunction createHelpButton()\n self.createButton({\n function_owner = self,\n click_function = \"toggleHelp\",\n position = Vector(0.845, 0.1, -0.855),\n height = 180,\n width = 180,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n })\nend\n\nfunction createInvestigatorButtons()\n local invButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = INVESTIGATOR_ROW_START:copy()\n for _, class in ipairs(CLASS_LIST) do\n invButtonParams.click_function = \"spawnInvestigators\" .. class\n invButtonParams.position = buttonPos\n self.createButton(invButtonParams)\n buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET\n self.setVar(invButtonParams.click_function, function(_, _, _) spawnInvestigatorGroup(class) end)\n end\nend\n\nfunction createLevelZeroButtons()\n local l0ButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = LEVEL_ZERO_ROW_START:copy()\n for _, class in ipairs(CLASS_LIST) do\n l0ButtonParams.click_function = \"spawnBasic\" .. class\n l0ButtonParams.position = buttonPos\n self.createButton(l0ButtonParams)\n buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET\n self.setVar(l0ButtonParams.click_function, function(_, _, _) spawnClassCards(class, false) end)\n end\nend\n\nfunction createUpgradedButtons()\n local upgradedButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = UPGRADED_ROW_START:copy()\n for _, class in ipairs(CLASS_LIST) do\n upgradedButtonParams.click_function = \"spawnUpgraded\" .. class\n upgradedButtonParams.position = buttonPos\n self.createButton(upgradedButtonParams)\n buttonPos.x = buttonPos.x + CLASS_BUTTONS_X_OFFSET\n self.setVar(upgradedButtonParams.click_function, function(_, _, _) spawnClassCards(class, true) end)\n end\nend\n\nfunction createWeaknessButtons()\n local weaknessButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = WEAKNESS_ROW_START:copy()\n weaknessButtonParams.click_function = \"spawnWeaknesses\"\n weaknessButtonParams.tooltip = \"All Weaknesses\"\n weaknessButtonParams.position = buttonPos\n self.createButton(weaknessButtonParams)\n\n buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET\n weaknessButtonParams.click_function = \"spawnRandomWeakness\"\n weaknessButtonParams.tooltip = \"Random Basic Weakness\\nRight-click to specify a trait\"\n weaknessButtonParams.position = buttonPos\n self.createButton(weaknessButtonParams)\nend\n\nfunction createOtherButtons()\n local otherButtonParams = {\n function_owner = self,\n height = CIRCLE_BUTTON_SIZE,\n width = CIRCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = OTHER_ROW_START:copy()\n otherButtonParams.click_function = \"spawnBonded\"\n otherButtonParams.tooltip = \"Bonded Cards\"\n otherButtonParams.position = buttonPos\n self.createButton(otherButtonParams)\n buttonPos.x = buttonPos.x + MISC_BUTTONS_X_OFFSET\n otherButtonParams.click_function = \"spawnUpgradeSheets\"\n otherButtonParams.tooltip = \"Customization Upgrade Sheets\"\n otherButtonParams.position = buttonPos\n self.createButton(otherButtonParams)\nend\n\nfunction createCycleButtons()\n local cycleButtonParams = {\n function_owner = self,\n height = CYCLE_BUTTON_SIZE,\n width = CYCLE_BUTTON_SIZE,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n }\n local buttonPos = CYCLE_BUTTON_START:copy()\n local rowCount = 0\n local colCount = 0\n for _, cycle in ipairs(CYCLE_LIST) do\n cycleButtonParams.click_function = \"spawnCycle\" .. cycle\n cycleButtonParams.position = buttonPos\n cycleButtonParams.tooltip = cycle\n self.createButton(cycleButtonParams)\n self.setVar(cycleButtonParams.click_function, function(_, _, _) spawnCycle(cycle) end)\n colCount = colCount + 1\n -- If we've reached the end of a row, shift down and back to the first column\n if colCount \u003e= CYCLE_COLUMN_COUNT then\n buttonPos = CYCLE_BUTTON_START:copy()\n rowCount = rowCount + 1\n colCount = 0\n buttonPos.z = buttonPos.z + CYCLE_BUTTONS_Z_OFFSET * rowCount\n if rowCount == 3 then\n -- Account for two centered buttons on the final row\n buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET / 2\n -- Account for centered button on the final row\n -- buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET\n end\n else\n buttonPos.x = buttonPos.x + CYCLE_BUTTONS_X_OFFSET\n end\n end\nend\n\nfunction createClearButton()\n self.createButton({\n function_owner = self,\n click_function = \"deleteAll\",\n position = Vector(0, 0.1, 0.852),\n height = 170,\n width = 750,\n scale = Vector(0.25, 1, 0.25),\n color = TRANSPARENT\n })\nend\n\nfunction createInvestigatorModeButtons()\n self.createButton({\n function_owner = self,\n click_function = \"setCardsOnlyMode\",\n position = Vector(0.251, 0.1, -0.322),\n height = 170,\n width = 760,\n scale = Vector(0.25, 1, 0.25),\n color = spawnStarterDecks and TRANSPARENT or STARTER_DECK_MODE_SELECTED_COLOR\n })\n self.createButton({\n function_owner = self,\n click_function = \"setspawnStarterDecks\",\n position = Vector(0.66, 0.1, -0.322),\n height = 170,\n width = 760,\n scale = Vector(0.25, 1, 0.25),\n color = spawnStarterDecks and STARTER_DECK_MODE_SELECTED_COLOR or TRANSPARENT\n })\n local checkX = spawnStarterDecks and 0.52 or 0.11\n self.createButton({\n function_owner = self,\n label = \"✓\",\n click_function = \"doNothing\",\n position = Vector(checkX, 0.11, -0.317),\n height = 0,\n width = 0,\n font_size = 300,\n scale = Vector(0.1, 1, 0.1),\n font_color = { 0, 0, 0 },\n color = { 1, 1, 1 }\n })\nend\n\nfunction createXML(showOtherCardsButton)\n -- basic XML for the help button\n local xmlTable = {\n {\n tag = \"Panel\",\n attributes = {\n active = \"false\",\n id = \"helpPanel\",\n position = \"-165 -70 -2\",\n rotation = \"0 0 180\",\n height = \"50\",\n width = \"107\",\n color = \"#00000099\"\n },\n children = {\n tag = \"Text\",\n attributes = {\n id = \"helpText\",\n rectAlignment = \"MiddleCenter\",\n height = \"480\",\n width = \"1000\",\n scale = \"0.1 0.1 1\",\n fontSize = \"66\",\n color = \"#F5F5F5\",\n backgroundColor = \"#FF0000\",\n alignment = \"MiddleLeft\",\n horizontalOverflow = \"wrap\",\n text = \"• Select a group to place cards\\n\" ..\n \"• Copy the cards you want for your deck\\n\" ..\n \"• Select a new group to clear the placed cards and see new ones\\n\" ..\n \"• Clear to remove all cards\"\n }\n }\n }\n }\n\n -- add the \"Additional Cards\" button if cards without cycle were detected\n if showOtherCardsButton then\n local otherCardsButtonXml = {\n tag = \"Panel\",\n attributes = {\n position = \"44.25 65.75 -11\",\n rotation = \"0 0 180\",\n height = \"225\",\n width = \"225\",\n scale = \"0.1 0.1 1\",\n onClick = \"spawnOtherCards\"\n },\n children = {\n tag = \"Image\",\n attributes = { image = \"OtherCards\" }\n }\n }\n table.insert(xmlTable, otherCardsButtonXml)\n end\n helpVisibleToPlayers = {}\n self.UI.setXmlTable(xmlTable)\nend\n\n-- click function for the XML button for the additional player cards\nfunction spawnOtherCards()\n spawnCycle(\"Other\")\nend\n\nfunction toggleHelp(_, playerColor, _)\n if helpVisibleToPlayers[playerColor] then\n helpVisibleToPlayers[playerColor] = nil\n else\n helpVisibleToPlayers[playerColor] = true\n end\n updateHelpVisibility()\nend\n\nfunction updateHelpVisibility()\n local visibility = \"\"\n for player, _ in pairs(helpVisibleToPlayers) do\n if string.len(visibility) \u003e 0 then\n visibility = visibility .. \"|\" .. player\n else\n visibility = player\n end\n end\n self.UI.setAttribute(\"helpText\", \"visibility\", visibility)\n self.UI.setAttribute(\"helpPanel\", \"visibility\", visibility)\n self.UI.setAttribute(\"helpPanel\", \"active\", string.len(visibility) \u003e 0)\nend\n\nfunction setspawnStarterDecks()\n spawnStarterDecks = true\n updateStarterModeButtons()\nend\n\nfunction setCardsOnlyMode()\n spawnStarterDecks = false\n updateStarterModeButtons()\nend\n\nfunction updateStarterModeButtons()\n local buttonCount = #self.getButtons()\n -- Buttons are 0-indexed, so the last three are -1, -2, and -3 from the size\n self.removeButton(buttonCount - 1)\n self.removeButton(buttonCount - 2)\n self.removeButton(buttonCount - 3)\n createInvestigatorModeButtons()\nend\n\n-- Clears the table and updates positions based on scale (should be called before ANY card placement)\nfunction prepareToPlaceCards()\n deleteAll()\n scalePositions()\nend\n\n-- Updates the positions based on the current object scale to ensure the relative layout functions\n-- properly at different scales.\nfunction scalePositions()\n -- Assume scaling is consistent in X and Z dimensions\n local scale = 1 / self.getScale().x\n startPositions = {}\n for key, pos in pairs(START_POSITIONS) do\n -- Because a scaled object means a different global size, using global distance for Z results in\n -- the cards being closer or farther depending on the scale. Leave the Z values and only scale X and Y\n startPositions[key] = Vector(pos)\n startPositions[key].x = startPositions[key].x * scale\n startPositions[key].y = startPositions[key].y * scale\n end\n cardRowOffset = CARD_ROW_OFFSET * scale\n cardGroupOffset = CARD_GROUP_OFFSET * scale\n investigatorPositionShiftRow = Vector(INVESTIGATOR_POSITION_SHIFT_ROW):scale(scale)\n investigatorPositionShiftCol = Vector(INVESTIGATOR_POSITION_SHIFT_COL):scale(scale)\n investigatorCardOffset = Vector(INVESTIGATOR_CARD_OFFSET):scale(scale)\n investigatorSignatureOffset = Vector(INVESTIGATOR_SIGNATURE_OFFSET):scale(scale)\nend\n\n-- Deletes all cards currently placed on the table\nfunction deleteAll()\n spawnBag.recall(true)\nend\n\n-- Spawn an investigator group, based on the current UI setting for either investigators or starter decks\n---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData\nfunction spawnInvestigatorGroup(groupName)\n prepareToPlaceCards()\n Wait.frames(function()\n if spawnStarterDecks then\n spawnStarters(groupName)\n else\n spawnInvestigators(groupName)\n end\n end, 2)\nend\n\n-- Spawn cards for all investigators in the given group. This creates piles for all defined\n-- investigator cards and minicards as well as the signature cards.\n---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData\nfunction spawnInvestigators(groupName)\n if INVESTIGATOR_GROUPS[groupName] == nil then\n printToAll(\"No investigator data for \" .. groupName .. \" yet\")\n return\n end\n\n local col = 1\n local row = 1\n local investigatorCount = #INVESTIGATOR_GROUPS[groupName]\n local position = getInvestigatorRowStartPos(investigatorCount, row)\n\n for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do\n for _, spawnSpec in ipairs(buildInvestigatorSpawnSpec(investigatorName, INVESTIGATORS[investigatorName], position)) do\n spawnBag.spawn(spawnSpec)\n end\n position:add(investigatorPositionShiftCol)\n col = col + 1\n if col \u003e INVESTIGATOR_MAX_COLS then\n col = 1\n row = row + 1\n position = getInvestigatorRowStartPos(investigatorCount, row)\n end\n end\nend\n\nfunction getInvestigatorRowStartPos(investigatorCount, row)\n local rowStart = Vector(startPositions.investigator)\n rowStart:add(Vector(\n investigatorPositionShiftRow.x * (row - 1),\n investigatorPositionShiftRow.y * (row - 1),\n investigatorPositionShiftRow.z * (row - 1)))\n local investigatorsInRow = math.min(investigatorCount - INVESTIGATOR_MAX_COLS * (row - 1), INVESTIGATOR_MAX_COLS)\n rowStart:add(Vector(\n investigatorPositionShiftCol.x * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,\n investigatorPositionShiftCol.y * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2,\n investigatorPositionShiftCol.z * (INVESTIGATOR_MAX_COLS - investigatorsInRow) / 2))\n return rowStart\nend\n\n-- Creates the spawn spec for the investigator's signature cards.\n---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData\n---@param investigatorData table Spawn definition for the investigator, retrieved from INVESTIGATORS\n---@param position tts__Vector Where to spawn the minicard; investigagor cards will be placed below\nfunction buildInvestigatorSpawnSpec(investigatorName, investigatorData, position)\n local sigPos = Vector(position):add(investigatorSignatureOffset)\n local spawns = buildCommonSpawnSpec(investigatorName, investigatorData, position)\n table.insert(spawns, {\n name = investigatorName .. \"signatures\",\n cards = investigatorData.signatures,\n globalPos = self.positionToWorld(sigPos),\n rotation = FACE_UP_ROTATION\n })\n\n return spawns\nend\n\n-- Builds the spawn specs for minicards and investigator cards. These are common enough to be\n-- shared, and will only differ in whether they spawn the full stack of possible investigator and\n-- minicards, or only the first of each.\n---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData\n---@param investigatorData table Spawn definition for the investigator, retrieved from INVESTIGATORS\n---@param position tts__Vector Where to spawn the minicard; investigagor cards will be placed below\n---@param oneCardOnly? boolean If true, will spawn only the first card in the investigator card\n--- and minicard lists. Otherwise, spawn them all in a deck\nfunction buildCommonSpawnSpec(investigatorName, investigatorData, position, oneCardOnly)\n local cardPos = Vector(position):add(investigatorCardOffset)\n return {\n {\n name = investigatorName .. \"minicards\",\n cards = oneCardOnly and { investigatorData.minicards[1] } or investigatorData.minicards,\n globalPos = self.positionToWorld(position),\n rotation = FACE_UP_ROTATION\n },\n {\n name = investigatorName .. \"cards\",\n cards = oneCardOnly and { investigatorData.cards[1] } or investigatorData.cards,\n globalPos = self.positionToWorld(cardPos),\n rotation = FACE_UP_ROTATION\n }\n }\nend\n\n-- Spawns all starter decks (single minicard and investigator card, plus the starter deck) for\n-- investigators in the given group.\n---@param groupName string Name of the group to spawn, matching a key in InvestigatorPanelData\nfunction spawnStarters(groupName)\n local col = 1\n local row = 1\n local investigatorCount = #INVESTIGATOR_GROUPS[groupName]\n local position = getInvestigatorRowStartPos(investigatorCount, row)\n for _, investigatorName in ipairs(INVESTIGATOR_GROUPS[groupName]) do\n spawnStarterDeck(investigatorName, INVESTIGATORS[investigatorName], position)\n position:add(investigatorPositionShiftCol)\n col = col + 1\n if col \u003e INVESTIGATOR_MAX_COLS then\n col = 1\n row = row + 1\n position = getInvestigatorRowStartPos(investigatorCount, row)\n end\n end\nend\n\n-- Spawns the defined starter deck for the given investigator's.\n---@param investigatorName string Name of the investigator, matching a key in InvestigatorPanelData\nfunction spawnStarterDeck(investigatorName, investigatorData, position)\n for _, spawnSpec in ipairs(buildCommonSpawnSpec(investigatorName, investigatorData, position, true)) do\n spawnBag.spawn(spawnSpec)\n end\n local deckPos = Vector(position):add(investigatorSignatureOffset)\n arkhamDb.getDecklist(\"None\", investigatorData.starterDeck, true, false, false, function(slots)\n local cardIdList = {}\n for id, count in pairs(slots) do\n for i = 1, count do\n table.insert(cardIdList, id)\n end\n end\n spawnBag.spawn({\n name = investigatorName .. \"starter\",\n cards = cardIdList,\n globalPos = self.positionToWorld(deckPos),\n rotation = FACE_DOWN_ROTATION\n })\n end)\nend\n\n-- Clears the currently placed cards, then places cards for the given class and level spread\n---@param cardClass string Class to place (\"Guardian\", \"Seeker\", etc)\n---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.\nfunction spawnClassCards(cardClass, isUpgraded)\n prepareToPlaceCards()\n Wait.frames(function() placeClassCards(cardClass, isUpgraded) end, 2)\nend\n\n-- Spawn the class cards.\n---@param cardClass string Class to place (\"Guardian\", \"Seeker\", etc)\n---@param isUpgraded boolean If true, spawn the Level 1-5 cards. Otherwise, Level 0.\nfunction placeClassCards(cardClass, isUpgraded)\n if not allCardsBagApi.isIndexReady() then return end\n\n local cardIdList = allCardsBagApi.getCardsByClassAndLevel(cardClass, isUpgraded)\n\n local skillList = {}\n local eventList = {}\n local assetList = {}\n for _, cardId in ipairs(cardIdList) do\n local cardMetadata = allCardsBagApi.getCardById(cardId).metadata\n if (cardMetadata.type == \"Skill\") then\n table.insert(skillList, cardId)\n elseif (cardMetadata.type == \"Event\") then\n table.insert(eventList, cardId)\n elseif (cardMetadata.type == \"Asset\") then\n table.insert(assetList, cardId)\n end\n end\n local groupPos = Vector(startPositions.classCards)\n if #skillList \u003e 0 then\n spawnBag.spawn({\n name = cardClass .. (isUpgraded and \"upgraded\" or \"basic\"),\n cards = skillList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#skillList / 20) * cardRowOffset + cardGroupOffset\n end\n if #eventList \u003e 0 then\n spawnBag.spawn({\n name = cardClass .. \"event\" .. (isUpgraded and \"upgraded\" or \"basic\"),\n cards = eventList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#eventList / 20) * cardRowOffset + cardGroupOffset\n end\n if #assetList \u003e 0 then\n spawnBag.spawn({\n name = cardClass .. \"asset\" .. (isUpgraded and \"upgraded\" or \"basic\"),\n cards = assetList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n end\nend\n\n-- Spawns the investigator sets and all cards for the given cycle\n---@param cycle string Name of a cycle, should match the standard used in card metadata\nfunction spawnCycle(cycle)\n if not allCardsBagApi.isIndexReady() then return end\n\n prepareToPlaceCards()\n spawnInvestigators(cycle)\n\n -- sort custom cards\n local sortByMetadata = false\n if cycle == \"Other\" then\n sortByMetadata = true\n end\n\n spawnBag.spawn({\n name = \"cycle\" .. cycle,\n cards = allCardsBagApi.getCardsByCycle(cycle, sortByMetadata),\n globalPos = self.positionToWorld(startPositions.cycle),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\nend\n\nfunction spawnBonded()\n prepareToPlaceCards()\n spawnBag.spawn({\n name = \"bonded\",\n cards = BONDED_CARD_LIST,\n globalPos = self.positionToWorld(startPositions.classCards),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\nend\n\nfunction spawnUpgradeSheets()\n prepareToPlaceCards()\n spawnBag.spawn({\n name = \"upgradeSheets\",\n cards = UPGRADE_SHEET_LIST,\n globalPos = self.positionToWorld(startPositions.classCards),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n spawnBag.spawn({\n name = \"servitor\",\n cards = { \"09080-m\" },\n globalPos = self.positionToWorld(startPositions.summonedServitor),\n rotation = FACE_UP_ROTATION,\n })\nend\n\n-- Clears the current cards, and places all basic weaknesses on the table.\nfunction spawnWeaknesses()\n if not allCardsBagApi.isIndexReady() then return end\n\n prepareToPlaceCards()\n\n local basicWeaknessList = {}\n local otherWeaknessList = {}\n for _, id in ipairs(allCardsBagApi.getUniqueWeaknesses()) do\n local cardMetadata = allCardsBagApi.getCardById(id).metadata\n if cardMetadata.basicWeaknessCount ~= nil and cardMetadata.basicWeaknessCount \u003e 0 then\n table.insert(basicWeaknessList, id)\n elseif excludedNonBasicWeaknesses[id] == nil then\n table.insert(otherWeaknessList, id)\n end\n end\n local groupPos = Vector(startPositions.classCards)\n spawnBag.spawn({\n name = \"basicWeaknesses\",\n cards = basicWeaknessList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#basicWeaknessList / 20) * cardRowOffset + cardGroupOffset\n spawnBag.spawn({\n name = \"evolvedWeaknesses\",\n cards = EVOLVED_WEAKNESSES,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\n groupPos.z = groupPos.z + math.ceil(#EVOLVED_WEAKNESSES / 20) * cardRowOffset + cardGroupOffset\n spawnBag.spawn({\n name = \"otherWeaknesses\",\n cards = otherWeaknessList,\n globalPos = self.positionToWorld(groupPos),\n rotation = FACE_UP_ROTATION,\n spread = true,\n spreadCols = 20\n })\nend\n\nfunction spawnRandomWeakness(_, playerColor, isRightClick)\n prepareToPlaceCards()\n\n if not isRightClick then\n local weaknessId = allCardsBagApi.getRandomWeaknessId()\n if weaknessId == nil then\n broadcastToAll(\"All basic weaknesses are in play!\", { 0.9, 0.2, 0.2 })\n else\n spawnSingleWeakness(weaknessId)\n end\n else\n Player[playerColor].showInputDialog(\"Specify a trait for the weakness (split multiple eligible traits with '|'):\", lastWeaknessTrait,\n function(text)\n lastWeaknessTrait = text\n local availableWeaknesses = allCardsBagApi.buildAvailableWeaknesses(text)\n if #availableWeaknesses \u003e 0 then\n spawnSingleWeakness(availableWeaknesses[math.random(#availableWeaknesses)])\n else\n broadcastToAll(\"No matching weakness available!\", { 0.9, 0.2, 0.2 })\n end\n end)\n end\nend\n\n-- spawn the random weakness\nfunction spawnSingleWeakness(weaknessId)\n spawnBag.spawn({\n name = \"randomWeakness\",\n cards = { weaknessId },\n globalPos = self.positionToWorld(startPositions.randomWeakness),\n rotation = FACE_UP_ROTATION,\n })\nend\nend)\n__bundle_register(\"playercards/AllCardsBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local AllCardsBagApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getAllCardsBag()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AllCardsBag\")\n end\n\n -- internal function to create a copy of the table to avoid operating on variables owned by different objects\n local function returnCopyOfList(data)\n local copiedList = {}\n for _, id in ipairs(data) do\n table.insert(copiedList, id)\n end\n return copiedList\n end\n\n -- Returns a specific card from the bag, based on ArkhamDB ID\n ---@param id string ID of the card to retrieve\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a single table with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardById = function(id)\n return getAllCardsBag().call(\"getCardById\", { id = id })\n end\n\n -- Gets a random basic weakness from the bag. Once a given ID has been returned it\n -- will be removed from the list and cannot be selected again until a reload occurs\n -- or the indexes are rebuilt, which will refresh the list to include all weaknesses.\n ---@return string: ID of the selected weakness\n AllCardsBagApi.getRandomWeaknessId = function()\n return getAllCardsBag().call(\"getRandomWeaknessId\")\n end\n\n AllCardsBagApi.isIndexReady = function()\n return getAllCardsBag().call(\"isIndexReady\")\n end\n\n -- Called by Hotfix bags when they load. If we are still loading indexes, then\n -- the all cards and hotfix bags are being loaded together, and we can ignore\n -- this call as the hotfix will be included in the initial indexing. If it is\n -- called once indexing is complete it means the hotfix bag has been added\n -- later, and we should rebuild the index to integrate the hotfix bag.\n AllCardsBagApi.rebuildIndexForHotfix = function()\n getAllCardsBag().call(\"rebuildIndexForHotfix\")\n end\n\n -- Searches the bag for cards which match the given name and returns a list.\n -- Note that this is an O(n) search without index support. It may be slow.\n ---@param name string or string fragment to search for names\n ---@param exact boolean Whether the name match should be exact\n AllCardsBagApi.getCardsByName = function(name, exact)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByName\", { name = name, exact = exact }))\n end\n\n AllCardsBagApi.isBagPresent = function()\n return getAllCardsBag() and true\n end\n\n -- Returns a list of cards from the bag matching a class and level (0 or upgraded)\n ---@param class string class to retrieve (\"Guardian\", \"Seeker\", etc)\n ---@param upgraded boolean True for upgraded cards (Level 1-5), false for Level 0\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByClassAndLevel = function(class, upgraded)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByClassAndLevel\", { class = class, upgraded = upgraded }))\n end\n\n -- Returns a list of cards from the bag matching a cycle\n ---@param cycle string Cycle to retrieve (\"The Scarlet Keys\" etc.)\n ---@param sortByMetadata boolean If true, sorts the table by metadata instead of ID\n ---@return table: If the indexes are still being constructed, returns an empty table.\n -- Otherwise, a list of tables, each with the following fields\n -- data: TTS object data, suitable for spawning the card\n -- metadata: Table of parsed metadata\n AllCardsBagApi.getCardsByCycle = function(cycle, sortByMetadata)\n return returnCopyOfList(getAllCardsBag().call(\"getCardsByCycle\", { cycle = cycle, sortByMetadata = sortByMetadata }))\n end\n\n -- Constructs a list of available basic weaknesses by starting with the full pool of basic\n -- weaknesses then removing any which are currently in the play or deck construction areas\n ---@param traits? string Trait(s) to use as filter\n ---@return table: Array of weakness IDs which are valid to choose from\n AllCardsBagApi.buildAvailableWeaknesses = function(traits)\n return returnCopyOfList(getAllCardsBag().call(\"buildAvailableWeaknesses\", traits))\n end\n\n AllCardsBagApi.getUniqueWeaknesses = function()\n return returnCopyOfList(getAllCardsBag().call(\"getUniqueWeaknesses\"))\n end\n\n return AllCardsBagApi\nend\nend)\n__bundle_register(\"playercards/PlayerCardPanelData\", function(require, _LOADED, __bundle_register, __bundle_modules)\nBONDED_CARD_LIST = {\n\t\"05314\", -- Soothing Melody\n\t\"06277\", -- Wish Eater\n\t\"06019\", -- Bloodlust\n\t\"06022\", -- Pendant of the Queen\n\t\"05317\", -- Blood-rite\n\t\"06113\", -- Essence of the Dream\n\t\"06028\", -- Stars Are Right\n\t\"06025\", -- Guardian of the Crystallizer\n\t\"06283\", -- Unbound Beast\n\t\"06032\", -- Zeal\n\t\"06031\", -- Hope\n\t\"06033\", -- Augur\n\t\"06331\", -- Dream Parasite\n\t\"06015a\", -- Dream-Gate\n\t\"10006\", -- Aetheric Current (Yuggoth)\n\t\"10007\", -- Aetheric Current (Yoth)\n\t\"10036\", -- Blade of Yoth\n\t\"10039\", -- Evanescent Ascension\n\t\"10045\", -- Uncanny Growth\n\t\"10063\", -- Bianca\n\t\"10086\", -- Rot\n\t\"10087\", -- Rot\n\t\"10088\", -- Rot\n\t\"10089\", -- Rot\n\t\"10090\", -- Rot\n\t\"10106\", -- Keeper of the Key\n\t\"10107\", -- Servant of Brass\n\t\"10134\", -- Twilight Diadem\n}\n\nUPGRADE_SHEET_LIST = {\n\t\"09040-c\", -- Alchemical Distillation\n\t\"09023-c\", -- Custom Modifications\n\t\"09059-c\", -- Damning Testimony\n\t\"09041-c\", -- Emperical Hypothesis\n\t\"09060-c\", -- Friends in Low Places\n\t\"09101-c\", -- Grizzled\n\t\"09061-c\", -- Honed Instinct\n\t\"09021-c\", -- Hunter's Armor\n\t\"09119-c\", -- Hyperphysical Shotcaster\n\t\"09079-c\", -- Living Ink\n\t\"09100-c\", -- Makeshift Trap\n\t\"09099-c\", -- Pocket Multi Tool\n\t\"09081-c\", -- Power Word\n\t\"09081-t-c\", -- Power Word (Taboo)\n\t\"09022-c\", -- Runic Axe\n\t\"09022-t-c\", -- Runic Axe (Taboo)\n\t\"09080-c\", -- Summoned Servitor\n\t\"09042-c\", -- Raven's Quill\n}\n\nEVOLVED_WEAKNESSES = {\n\t\"04039\",\n\t\"04041\",\n\t\"04042\",\n\t\"53014\",\n\t\"53015\",\n}\n\n------------------ START INVESTIGATOR DATA DEFINITION ------------------\nINVESTIGATOR_GROUPS = {\n\t[\"Guardian\"] = {\n\t\t\"Roland Banks\",\n\t\t\"Zoey Samaras\",\n\t\t\"Mark Harrigan\",\n\t\t\"Leo Anderson\",\n\t\t\"Carolyn Fern\",\n\t\t\"Tommy Muldoon\",\n\t\t\"Nathaniel Cho\",\n\t\t\"Sister Mary\",\n\t\t\"Daniela Reyes\",\n\t\t\"Carson Sinclair\",\n\t\t\"Wilson Richards\"\n\t},\n\t[\"Seeker\"] = {\n\t\t\"Daisy Walker\",\n\t\t\"Rex Murphy\",\n\t\t\"Minh Thi Phan\",\n\t\t\"Ursula Downs\",\n\t\t\"Joe Diamond\",\n\t\t\"Mandy Thompson\",\n\t\t\"Harvey Walters\",\n\t\t\"Amanda Sharpe\",\n\t\t\"Norman Withers\",\n\t\t\"Vincent Lee\",\n\t\t\"Kate Winthrop\"\n\t},\n\t[\"Rogue\"] = {\n\t\t\"\\\"Skids\\\" O'Toole\",\n\t\t\"Jenny Barnes\",\n\t\t\"Sefina Rousseau\",\n\t\t\"Finn Edwards\",\n\t\t\"Preston Fairmont\",\n\t\t\"Tony Morgan\",\n\t\t\"Winifred Habbamock\",\n\t\t\"Trish Scarborough\",\n\t\t\"Monterey Jack\",\n\t\t\"Kymani Jones\",\n\t\t\"Alessandra Zorzi\"\n\t},\n\t[\"Mystic\"] = {\n\t\t\"Agnes Baker\",\n\t\t\"Jim Culver\",\n\t\t\"Akachi Onyele\",\n\t\t\"Father Mateo\",\n\t\t\"Diana Stanley\",\n\t\t\"Marie Lambeau\",\n\t\t\"Luke Robinson\",\n\t\t\"Jacqueline Fine\",\n\t\t\"Dexter Drake\",\n\t\t\"Lily Chen\",\n\t\t\"Amina Zidane\",\n\t\t\"Gloria Goldberg\",\n\t\t\"Kōhaku Narukami\"\n\t},\n\t[\"Survivor\"] = {\n\t\t\"Wendy Adams\",\n\t\t\"\\\"Ashcan\\\" Pete\",\n\t\t\"William Yorick\",\n\t\t\"Calvin Wright\",\n\t\t\"Rita Young\",\n\t\t\"Patrice Hathaway\",\n\t\t\"Stella Clark\",\n\t\t\"Silas Marsh\",\n\t\t\"Bob Jenkins\",\n\t\t\"Darrell Simmons\",\n\t\t\"Hank Samson\"\n\t},\n\t[\"Neutral\"] = {\n\t\t\"Lola Hayes\",\n\t\t\"Charlie Kane\",\n\t\t\"Subject 5U-21\"\n\t},\n\t[\"Core\"] = {\n\t\t\"Roland Banks\",\n\t\t\"Daisy Walker\",\n\t\t\"\\\"Skids\\\" O'Toole\",\n\t\t\"Agnes Baker\",\n\t\t\"Wendy Adams\"\n\t},\n\t[\"The Dunwich Legacy\"] = {\n\t\t\"Zoey Samaras\",\n\t\t\"Rex Murphy\",\n\t\t\"Jenny Barnes\",\n\t\t\"Jim Culver\",\n\t\t\"\\\"Ashcan\\\" Pete\"\n\t},\n\t[\"The Path to Carcosa\"] = {\n\t\t\"Mark Harrigan\",\n\t\t\"Minh Thi Phan\",\n\t\t\"Sefina Rousseau\",\n\t\t\"Akachi Onyele\",\n\t\t\"William Yorick\",\n\t\t\"Lola Hayes\"\n\t},\n\t[\"The Forgotten Age\"] = {\n\t\t\"Leo Anderson\",\n\t\t\"Ursula Downs\",\n\t\t\"Finn Edwards\",\n\t\t\"Father Mateo\",\n\t\t\"Calvin Wright\"\n\t},\n\t[\"The Circle Undone\"] = {\n\t\t\"Carolyn Fern\",\n\t\t\"Joe Diamond\",\n\t\t\"Preston Fairmont\",\n\t\t\"Diana Stanley\",\n\t\t\"Rita Young\",\n\t\t\"Marie Lambeau\"\n\t},\n\t[\"The Dream-Eaters\"] = {\n\t\t\"Tommy Muldoon\",\n\t\t\"Mandy Thompson\",\n\t\t\"Tony Morgan\",\n\t\t\"Luke Robinson\",\n\t\t\"Patrice Hathaway\"\n\t},\n\t[\"Investigator Packs\"] = {\n\t\t\"Nathaniel Cho\",\n\t\t\"Harvey Walters\",\n\t\t\"Winifred Habbamock\",\n\t\t\"Jacqueline Fine\",\n\t\t\"Stella Clark\",\n\t\t\"Gloria Goldberg\"\n\t},\n\t[\"The Innsmouth Conspiracy\"] = {\n\t\t\"Sister Mary\",\n\t\t\"Amanda Sharpe\",\n\t\t\"Trish Scarborough\",\n\t\t\"Dexter Drake\",\n\t\t\"Silas Marsh\"\n\t},\n\t[\"Edge of the Earth\"] = {\n\t\t\"Daniela Reyes\",\n\t\t\"Norman Withers\",\n\t\t\"Monterey Jack\",\n\t\t\"Lily Chen\",\n\t\t\"Bob Jenkins\"\n\t},\n\t[\"The Scarlet Keys\"] = {\n\t\t\"Carson Sinclair\",\n\t\t\"Vincent Lee\",\n\t\t\"Kymani Jones\",\n\t\t\"Amina Zidane\",\n\t\t\"Darrell Simmons\",\n\t\t\"Charlie Kane\"\n\t},\n\t[\"The Feast of Hemlock Vale\"] = {\n\t\t\"Wilson Richards\",\n\t\t\"Kate Winthrop\",\n\t\t\"Alessandra Zorzi\",\n\t\t\"Kōhaku Narukami\",\n\t\t\"Hank Samson\"\n\t}\n}\n\nINVESTIGATORS = {}\n-- Core Box\nINVESTIGATORS[\"Roland Banks\"] = {\n\tcards = { \"01001\", \"01001-p\", \"01001-pf\", \"01001-pb\" },\n\tminicards = { \"01001-m\" },\n\tsignatures = { \"01006\", \"01007\", \"90030\", \"90031\", \"90025\", \"90026\", \"90027\", \"90028\", \"90029\", \"98005\", \"98006\" },\n\tstarterDeck = \"2624931\"\n}\nINVESTIGATORS[\"Daisy Walker\"] = {\n\tcards = { \"01002\", \"01002-p\", \"01002-pf\", \"01002-pb\" },\n\tminicards = { \"01002-m\" },\n\tsignatures = { \"01008\", \"01009\", \"90002\", \"90003\" },\n\tstarterDeck = \"2624938\"\n}\nINVESTIGATORS[\"\\\"Skids\\\" O'Toole\"] = {\n\tcards = { \"01003\", \"01003-p\", \"01003-pf\", \"01003-pb\" },\n\tminicards = { \"01003-m\" },\n\tsignatures = { \"01010\", \"01011\", \"90009\", \"90010\" },\n\tstarterDeck = \"2624940\"\n}\nINVESTIGATORS[\"Agnes Baker\"] = {\n\tcards = { \"01004\", \"01004-p\", \"01004-pf\", \"01004-pb\" },\n\tminicards = { \"01004-m\" },\n\tsignatures = { \"01012\", \"01013\", \"90018\", \"90019\" },\n\tstarterDeck = \"2624944\"\n}\nINVESTIGATORS[\"Wendy Adams\"] = {\n\tcards = { \"01005\", \"01005-p\", \"01005-pf\", \"01005-pb\" },\n\tminicards = { \"01005-m\" },\n\tsignatures = { \"01014\", \"01015\", \"01515\", \"90039\", \"90040\", \"90038\" },\n\tstarterDeck = \"2624949\"\n}\n-- The Dunwich Legacy\nINVESTIGATORS[\"Zoey Samaras\"] = {\n\tcards = { \"02001\", \"02001-p\", \"02001-pf\", \"02001-pb\" },\n\tminicards = { \"02001-m\" },\n\tsignatures = { \"02006\", \"02007\", \"90060\", \"90061\" },\n\tstarterDeck = \"2624950\"\n}\nINVESTIGATORS[\"Rex Murphy\"] = {\n\tcards = { \"02002\", \"02002-t\", \"02002-p\", \"02002-pf\", \"02002-pb\" },\n\tminicards = { \"02002-m\" },\n\tsignatures = { \"02008\", \"02009\", \"90079\", \"90080\" },\n\tstarterDeck = \"2624958\"\n}\nINVESTIGATORS[\"Jenny Barnes\"] = {\n\tcards = { \"02003\" },\n\tminicards = { \"02003-m\" },\n\tsignatures = { \"02010\", \"02011\", \"98002\", \"98003\" },\n\tstarterDeck = \"2624961\"\n}\nINVESTIGATORS[\"Jim Culver\"] = {\n\tcards = { \"02004\", \"02004-p\", \"02004-pf\", \"02004-pb\" },\n\tminicards = { \"02004-m\" },\n\tsignatures = { \"02012\", \"02013\", \"90050\", \"90051\", \"90052\", \"90053\" },\n\tstarterDeck = \"2624965\"\n}\nINVESTIGATORS[\"\\\"Ashcan\\\" Pete\"] = {\n\tcards = { \"02005\", \"02005-p\", \"02005-pf\", \"02005-pb\" },\n\tminicards = { \"02005-m\" },\n\tsignatures = { \"02014\", \"02015\", \"90047\", \"90048\" },\n\tstarterDeck = \"2624969\"\n}\n-- The Path to Carcosa\nINVESTIGATORS[\"Mark Harrigan\"] = {\n\tcards = { \"03001\" },\n\tminicards = { \"03001-m\" },\n\tsignatures = { \"03007\", \"03008\", \"03009\" },\n\tstarterDeck = \"2624975\"\n}\nINVESTIGATORS[\"Minh Thi Phan\"] = {\n\tcards = { \"03002\" },\n\tminicards = { \"03002-m\" },\n\tsignatures = { \"03010\", \"03011\" },\n\tstarterDeck = \"2624979\"\n}\nINVESTIGATORS[\"Sefina Rousseau\"] = {\n\tcards = { \"03003\" },\n\tminicards = { \"03003-m\" },\n\tsignatures = { \"03012\", \"03012\", \"03012\", \"03013\" },\n\tstarterDeck = \"2624981\"\n}\nINVESTIGATORS[\"Akachi Onyele\"] = {\n\tcards = { \"03004\" },\n\tminicards = { \"03004-m\" },\n\tsignatures = { \"03014\", \"03015\" },\n\tstarterDeck = \"2624984\"\n}\nINVESTIGATORS[\"William Yorick\"] = {\n\tcards = { \"03005\" },\n\tminicards = { \"03005-m\" },\n\tsignatures = { \"03016\", \"03017\" },\n\tstarterDeck = \"2624988\"\n}\nINVESTIGATORS[\"Lola Hayes\"] = {\n\tcards = { \"03006\", \"03006-t\" },\n\tminicards = { \"03006-m\" },\n\tsignatures = { \"03018\", \"03018\", \"03019\", \"03019\", \"03019-t\", \"03019-t\" },\n\tstarterDeck = \"2624990\"\n}\n-- The Forgotten Age\nINVESTIGATORS[\"Leo Anderson\"] = {\n\tcards = { \"04001\" },\n\tminicards = { \"04001-m\" },\n\tsignatures = { \"04006\", \"04007\" },\n\tstarterDeck = \"2624994\"\n}\nINVESTIGATORS[\"Ursula Downs\"] = {\n\tcards = { \"04002\" },\n\tminicards = { \"04002-m\" },\n\tsignatures = { \"04008\", \"04009\" },\n\tstarterDeck = \"2625000\"\n}\nINVESTIGATORS[\"Finn Edwards\"] = {\n\tcards = { \"04003\" },\n\tminicards = { \"04003-m\" },\n\tsignatures = { \"04010\", \"04011\", \"04012\" },\n\tstarterDeck = \"2625003\"\n}\nINVESTIGATORS[\"Father Mateo\"] = {\n\tcards = { \"04004\" },\n\tminicards = { \"04004-m\" },\n\tsignatures = { \"04013\", \"04014\" },\n\tstarterDeck = \"2625005\"\n}\nINVESTIGATORS[\"Calvin Wright\"] = {\n\tcards = { \"04005\" },\n\tminicards = { \"04005-m\" },\n\tsignatures = { \"04015\", \"04016\" },\n\tstarterDeck = \"2625007\"\n}\n-- The Circle Undone\nINVESTIGATORS[\"Carolyn Fern\"] = {\n\tcards = { \"05001\" },\n\tminicards = { \"05001-m\" },\n\tsignatures = { \"05007\", \"05008\", \"98011\", \"98012\" },\n\tstarterDeck = \"2626342\"\n}\nINVESTIGATORS[\"Joe Diamond\"] = {\n\tcards = { \"05002\" },\n\tminicards = { \"05002-m\" },\n\tsignatures = { \"05009\", \"05010\" },\n\tstarterDeck = \"2626347\"\n}\nINVESTIGATORS[\"Preston Fairmont\"] = {\n\tcards = { \"05003\" },\n\tminicards = { \"05003-m\" },\n\tsignatures = { \"05011\", \"05012\" },\n\tstarterDeck = \"2626365\"\n}\nINVESTIGATORS[\"Diana Stanley\"] = {\n\tcards = { \"05004\" },\n\tminicards = { \"05004-m\" },\n\tsignatures = { \"05013\", \"05014\", \"05015\" },\n\tstarterDeck = \"2626385\"\n}\nINVESTIGATORS[\"Rita Young\"] = {\n\tcards = { \"05005\" },\n\tminicards = { \"05005-m\" },\n\tsignatures = { \"05016\", \"05017\" },\n\tstarterDeck = \"2626387\"\n}\nINVESTIGATORS[\"Marie Lambeau\"] = {\n\tcards = { \"05006\" },\n\tminicards = { \"05006-m\" },\n\tsignatures = { \"05018\", \"05019\" },\n\tstarterDeck = \"2626395\"\n}\n-- The Dream-Eaters\nINVESTIGATORS[\"Tommy Muldoon\"] = {\n\tcards = { \"06001\" },\n\tminicards = { \"06001-m\" },\n\tsignatures = { \"06006\", \"06007\" },\n\tstarterDeck = \"2626402\"\n}\nINVESTIGATORS[\"Mandy Thompson\"] = {\n\tcards = { \"06002\", \"06002-t\" },\n\tminicards = { \"06002-m\" },\n\tsignatures = { \"06008\", \"06008\", \"06008\", \"06009\" },\n\tstarterDeck = \"2626410\"\n}\nINVESTIGATORS[\"Tony Morgan\"] = {\n\tcards = { \"06003\" },\n\tminicards = { \"06003-m\" },\n\tsignatures = { \"06010\", \"06011\", \"06011\", \"06012\" },\n\tstarterDeck = \"2626446\"\n}\nINVESTIGATORS[\"Luke Robinson\"] = {\n\tcards = { \"06004\" },\n\tminicards = { \"06004-m\" },\n\tsignatures = { \"06013\", \"06014\", \"06015\" },\n\tstarterDeck = \"2626452\"\n}\nINVESTIGATORS[\"Patrice Hathaway\"] = {\n\tcards = { \"06005\" },\n\tminicards = { \"06005-m\" },\n\tsignatures = { \"06016\", \"06017\" },\n\tstarterDeck = \"2626461\"\n}\n-- Starter Decks\nINVESTIGATORS[\"Nathaniel Cho\"] = {\n\tcards = { \"60101\" },\n\tminicards = { \"60101-m\" },\n\tsignatures = { \"60102\", \"60103\" },\n\tstarterDeck = \"2643925\"\n}\nINVESTIGATORS[\"Harvey Walters\"] = {\n\tcards = { \"60201\" },\n\tminicards = { \"60201-m\" },\n\tsignatures = { \"60202\", \"60203\" },\n\tstarterDeck = \"2643928\"\n}\nINVESTIGATORS[\"Winifred Habbamock\"] = {\n\tcards = { \"60301\" },\n\tminicards = { \"60301-m\" },\n\tsignatures = { \"60302\", \"60303\" },\n\tstarterDeck = \"2643931\"\n}\nINVESTIGATORS[\"Jacqueline Fine\"] = {\n\tcards = { \"60401\" },\n\tminicards = { \"60401-m\" },\n\tsignatures = { \"60402\", \"60403\" },\n\tstarterDeck = \"2643932\"\n}\nINVESTIGATORS[\"Stella Clark\"] = {\n\tcards = { \"60501\" },\n\tminicards = { \"60501-m\" },\n\tsignatures = { \"60502\", \"60502\", \"60502\", \"60503\" },\n\tstarterDeck = \"2643934\"\n}\n-- The Innsmouth Conspiracy\nINVESTIGATORS[\"Sister Mary\"] = {\n\tcards = { \"07001\" },\n\tminicards = { \"07001-m\" },\n\tsignatures = { \"07006\", \"07007\" },\n\tstarterDeck = \"2626464\"\n}\nINVESTIGATORS[\"Amanda Sharpe\"] = {\n\tcards = { \"07002\" },\n\tminicards = { \"07002-m\" },\n\tsignatures = { \"07008\", \"07009\" },\n\tstarterDeck = \"2626469\"\n}\nINVESTIGATORS[\"Trish Scarborough\"] = {\n\tcards = { \"07003\", \"07003-t\" },\n\tminicards = { \"07003-m\" },\n\tsignatures = { \"07010\", \"07011\" },\n\tstarterDeck = \"2626479\"\n}\nINVESTIGATORS[\"Dexter Drake\"] = {\n\tcards = { \"07004\" },\n\tminicards = { \"07004-m\" },\n\tsignatures = { \"07012\", \"07013\", \"98017\", \"98018\" },\n\tstarterDeck = \"2626672\"\n}\nINVESTIGATORS[\"Silas Marsh\"] = {\n\tcards = { \"07005\" },\n\tminicards = { \"07005-m\" },\n\tsignatures = { \"07014\", \"07015\", \"07016\", \"98014\", \"98015\" },\n\tstarterDeck = \"2626685\"\n}\n-- Edge of the Earth\nINVESTIGATORS[\"Daniela Reyes\"] = {\n\tcards = { \"08001\" },\n\tminicards = { \"08001-m\" },\n\tsignatures = { \"08002\", \"08003\" },\n\tstarterDeck = \"2634588\"\n}\nINVESTIGATORS[\"Norman Withers\"] = {\n\tcards = { \"08004\" },\n\tminicards = { \"08004-m\" },\n\tsignatures = { \"08005\", \"08006\", \"98008\", \"98009\" },\n\tstarterDeck = \"2634603\"\n}\nINVESTIGATORS[\"Monterey Jack\"] = {\n\tcards = { \"08007\", \"08007-p\", \"08007-pf\", \"08007-pb\" },\n\tminicards = { \"08007-m\" },\n\tsignatures = { \"08008\", \"08009\", \"90063\", \"90064\" },\n\tstarterDeck = \"2634652\"\n}\nINVESTIGATORS[\"Lily Chen\"] = {\n\tcards = { \"08010\" },\n\tminicards = { \"08010-m\" },\n\tsignatures = { \"08011a\", \"08012a\", \"08013a\", \"08014a\", \"08015\", \"08015\", \"08015\", \"08015\" },\n\tstarterDeck = \"2634658\"\n}\nINVESTIGATORS[\"Bob Jenkins\"] = {\n\tcards = { \"08016\" },\n\tminicards = { \"08016-m\" },\n\tsignatures = { \"08017\", \"08018\" },\n\tstarterDeck = \"2634643\"\n}\n-- The Scarlet Keys\nINVESTIGATORS[\"Carson Sinclair\"] = {\n\tcards = { \"09001\" },\n\tminicards = { \"09001-m\" },\n\tsignatures = { \"09002\", \"09002\", \"09003\" },\n\tstarterDeck = \"2634667\"\n}\nINVESTIGATORS[\"Vincent Lee\"] = {\n\tcards = { \"09004\" },\n\tminicards = { \"09004-m\" },\n\tsignatures = { \"09005\", \"09006\", \"09006\", \"09006\", \"09006\", \"09007\" },\n\tstarterDeck = \"2634675\"\n}\nINVESTIGATORS[\"Kymani Jones\"] = {\n\tcards = { \"09008\" },\n\tminicards = { \"09008-m\" },\n\tsignatures = { \"09009\", \"09010\" },\n\tstarterDeck = \"2634701\"\n}\nINVESTIGATORS[\"Amina Zidane\"] = {\n\tcards = { \"09011\" },\n\tminicards = { \"09011-m\" },\n\tsignatures = { \"09012\", \"09013\", \"09014\" },\n\tstarterDeck = \"2634697\"\n}\nINVESTIGATORS[\"Darrell Simmons\"] = {\n\tcards = { \"09015\" },\n\tminicards = { \"09015-m\" },\n\tsignatures = { \"09016\", \"09017\" },\n\tstarterDeck = \"2634757\"\n}\nINVESTIGATORS[\"Charlie Kane\"] = {\n\tcards = { \"09018\" },\n\tminicards = { \"09018-m\" },\n\tsignatures = { \"09019\", \"09020\" },\n\tstarterDeck = \"2634706\"\n}\n-- The Feast of Hemlock Vale\nINVESTIGATORS[\"Wilson Richards\"] = {\n\tcards = { \"10001\" },\n\tminicards = { \"10001-m\" },\n\tsignatures = { \"10002\", \"10003\" },\n\tstarterDeck = \"3893753\"\n}\nINVESTIGATORS[\"Kate Winthrop\"] = {\n\tcards = { \"10004\" },\n\tminicards = { \"10004-m\" },\n\tsignatures = { \"10005\", \"10006\", \"10007\", \"10008\" },\n\tstarterDeck = \"3893779\"\n}\nINVESTIGATORS[\"Alessandra Zorzi\"] = {\n\tcards = { \"10009\" },\n\tminicards = { \"10009-m\" },\n\tsignatures = { \"10010\", \"10010\", \"10010\", \"10011\" },\n\tstarterDeck = \"3893775\"\n}\nINVESTIGATORS[\"Kōhaku Narukami\"] = {\n\tcards = { \"10012\" },\n\tminicards = { \"10012-m\" },\n\tsignatures = { \"10013\", \"10014\" },\n\tstarterDeck = \"3893763\"\n}\nINVESTIGATORS[\"Hank Samson\"] = {\n\tcards = { \"10015\", \"10015-b1\", \"10015-b2\" },\n\tminicards = { \"10015-m\" },\n\tsignatures = { \"10017\", \"10018\" },\n\tstarterDeck = \"3893788\"\n}\n-- PnP content\nINVESTIGATORS[\"Subject 5U-21\"] = {\n\tcards = { \"89001\" },\n\tminicards = { \"89001-m\" },\n\tsignatures = { \"89002\", \"89003\", \"89003\", \"89003\", \"89004\", \"89004\", \"89004\", \"89005\" },\n\tstarterDeck = \"3893795\"\n}\n-- Promo content\nINVESTIGATORS[\"Gloria Goldberg\"] = {\n\tcards = { \"98019\" },\n\tminicards = { \"98019-m\" },\n\tsignatures = { \"98020\", \"98021\" },\n\tstarterDeck = \"2636199\"\n}\n------------------ END INVESTIGATOR DATA DEFINITION ------------------\nend)\n__bundle_register(\"playercards/SpawnBag\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/PlayerCardSpawner\")\n\n-- Allows spawning of defined lists of cards which will be created from the template in the All\n-- Player Cards bag. SpawnBag.spawn will create objects based on a table definition, while\n-- SpawnBag.recall will clean them all up. Recall will be limited to a small area around the\n-- spawned objects. Objects moved out of this area will not be cleaned up.\n--\n-- SpawnSpec: Spawning requires a spawn specification with the following structure:\n-- {\n-- name: Name of this spawn content, used for internal tracking. Multiple specs can be spawned,\n-- but each requires a separate name\n-- cards: A list of card IDs to be spawned\n-- globalPos: Where the spawned objects should be placed, in global coordinates. This should be\n-- a valid Vector with x, y, and z defined, e.g. { x = 5, y = 1, z = 15 }\n-- rotation: Rotation for the spawned objects. X=180 should be used for face down items. As with\n-- globalPos, this should be a valid Vector with x, y, and z defined\n-- spread: Optional Boolean. If present and true, cards will be spawned next to each other in a\n-- spread moving to the right. globalPos will define the location of the first card, each\n-- after that will be moved a predefined distance\n-- spreadCols: Optional integer. If spread is true, specifies the maximum columns cards will be\n-- laid out in before starting a new row. If spread is true but spreadCols is not set, all\n-- cards will be in a single row (however long that may be)\n-- }\n-- See BondedBag.ttslua for an example\ndo\n local allCardsBagApi = require(\"playercards/AllCardsBagApi\")\n\n local SpawnBag = {}\n local internal = {}\n\n -- To assist debugging, will draw a box around the recall zone when it's set up\n local SHOW_RECALL_ZONE = false\n\n -- Distance to expand the recall zone around any added object.\n local RECALL_BUFFER_X = 0.9\n local RECALL_BUFFER_Z = 0.5\n\n -- In order to mimic the behavior of the previous memory buttons we use a temporary bag when\n -- recalling objects. This bag is tiny and transparent, and will be placed at the same location as\n -- this object. Once all placed cards are recalled bag to this bag, it will be destroyed\n local RECALL_BAG = {\n Name = \"Bag\",\n Transform = {\n scaleX = 0.01,\n scaleY = 0.01,\n scaleZ = 0.01,\n },\n ColorDiffuse = {\n r = 0,\n g = 0,\n b = 0,\n a = 0,\n },\n Locked = true,\n Grid = true,\n Snap = false,\n Tooltip = false\n }\n\n -- Tracks what has been placed by this \"bag\" so they can be recalled\n local placedSpecs = {}\n local placedObjectGuids = {}\n local recallZone = nil\n\n -- Loads a table of saved state, extracted during the parent object's onLoad\n SpawnBag.loadFromSave = function(saveTable)\n placedSpecs = saveTable.placed\n placedObjectGuids = saveTable.placedObjects\n recallZone = saveTable.recall\n end\n\n -- Generates a table of save state that can be included in the parent object's onSave\n SpawnBag.getStateForSave = function()\n return {\n placed = placedSpecs,\n placedObjects = placedObjectGuids,\n recall = recallZone,\n }\n end\n\n -- Places the given spawnSpec on the table. See comment at the start of the file for spawnSpec table data and examples\n SpawnBag.spawn = function(spawnSpec)\n -- Limit to one placement at a time\n if placedSpecs[spawnSpec.name] or spawnSpec == nil then return end\n\n local cardsToSpawn = {}\n for _, cardId in ipairs(spawnSpec.cards) do\n local card = allCardsBagApi.getCardById(cardId)\n if card ~= nil then\n table.insert(cardsToSpawn, card)\n end\n end\n if spawnSpec.spread then\n Spawner.spawnCardSpread(cardsToSpawn, spawnSpec.globalPos, spawnSpec.spreadCols or 9999, spawnSpec.rotation, false, internal.recordPlacedObject)\n else\n -- TTS decks come out in reverse order of the cards, reverse the list so the input order stays\n -- This only applies for decks; spreads are spawned by us in the order given\n if spawnSpec.rotation.z ~= 180 then\n cardsToSpawn = internal.reverseList(cardsToSpawn)\n end\n Spawner.spawnCards(cardsToSpawn, spawnSpec.globalPos, spawnSpec.rotation, false, internal.recordPlacedObject)\n end\n placedSpecs[spawnSpec.name] = true\n end\n\n -- Recalls all spawned objects to the bag, and clears the placedObjectGuids list\n ---@param fast boolean If true, cards will be deleted directly without faking the bag recall.\n SpawnBag.recall = function(fast)\n if fast then\n internal.deleteSpawned()\n else\n internal.recallSpawned()\n end\n\n -- We've recalled everything we can, some cards may have been moved out of the card area. Just reset at this point.\n placedSpecs = {}\n placedObjectGuids = {}\n recallZone = nil\n end\n\n -- Delete all spawned cards\n internal.deleteSpawned = function()\n for guid, _ in pairs(placedObjectGuids) do\n local obj = getObjectFromGUID(guid)\n if (obj ~= nil) then\n if (internal.isInRecallZone(obj)) then\n obj.destruct()\n end\n placedObjectGuids[guid] = nil\n end\n end\n end\n\n -- Recalls spawned cards with a fake bag that replicates the memory bag recall style\n internal.recallSpawned = function()\n local trash = spawnObjectData({ data = RECALL_BAG, position = self.getPosition() })\n for guid, _ in pairs(placedObjectGuids) do\n local obj = getObjectFromGUID(guid)\n if (obj ~= nil) then\n if (internal.isInRecallZone(obj)) then\n trash.putObject(obj)\n end\n placedObjectGuids[guid] = nil\n end\n end\n\n trash.destruct()\n end\n\n -- Callback for when an object has been spawned. Tracks the object for later recall and updates the recall zone.\n internal.recordPlacedObject = function(spawned)\n placedObjectGuids[spawned.getGUID()] = true\n internal.expandRecallZone(spawned)\n end\n\n -- Expands the current recall zone based on the position of the given object. The recall zone will\n -- be maintained as the bounding box of the extreme object positions, plus a small amount of buffer\n internal.expandRecallZone = function(spawnedCard)\n local pos = spawnedCard.getPosition()\n if (recallZone == nil) then\n -- First card out of the bag, initialize surrounding that\n recallZone = {}\n recallZone.upperLeft = { x = pos.x + RECALL_BUFFER_X, z = pos.z + RECALL_BUFFER_Z }\n recallZone.lowerRight = { x = pos.x - RECALL_BUFFER_X, z = pos.z - RECALL_BUFFER_Z }\n return\n end\n\n if pos.x \u003e recallZone.upperLeft.x then\n recallZone.upperLeft.x = pos.x + RECALL_BUFFER_X\n end\n if pos.x \u003c recallZone.lowerRight.x then\n recallZone.lowerRight.x = pos.x - RECALL_BUFFER_X\n end\n if pos.z \u003e recallZone.upperLeft.z then\n recallZone.upperLeft.z = pos.z + RECALL_BUFFER_Z\n end\n if pos.z \u003c recallZone.lowerRight.z then\n recallZone.lowerRight.z = pos.z - RECALL_BUFFER_Z\n end\n\n if SHOW_RECALL_ZONE then\n local y = 1.5\n local thick = 0.05\n Global.setVectorLines({\n {\n points = { { recallZone.upperLeft.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.lowerRight.z } },\n color = { 1, 0, 0 },\n thickness = thick,\n rotation = { 0, 0, 0 }\n },\n {\n points = { { recallZone.upperLeft.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.lowerRight.z } },\n color = { 1, 0, 0 },\n thickness = thick,\n rotation = { 0, 0, 0 }\n },\n {\n points = { { recallZone.lowerRight.x, y, recallZone.lowerRight.z }, { recallZone.lowerRight.x, y, recallZone.upperLeft.z } },\n color = { 1, 0, 0 },\n thickness = thick,\n rotation = { 0, 0, 0 }\n },\n {\n points = { { recallZone.lowerRight.x, y, recallZone.upperLeft.z }, { recallZone.upperLeft.x, y, recallZone.upperLeft.z } },\n color = { 1, 0, 0 },\n thickness = thick,\n rotation = { 0, 0, 0 }\n }\n })\n end\n end\n\n -- Checks to see if the given object is in the current recall zone. If there isn't a recall zone,\n -- will return true so that everything can be easily cleaned up.\n internal.isInRecallZone = function(obj)\n if (recallZone == nil) then\n return true\n end\n local pos = obj.getPosition()\n return (pos.x \u003c recallZone.upperLeft.x and pos.x \u003e recallZone.lowerRight.x\n and pos.z \u003c recallZone.upperLeft.z and pos.z \u003e recallZone.lowerRight.z)\n end\n\n internal.reverseList = function(list)\n local reversed = {}\n for i = 1, #list do\n reversed[i] = list[#list - i + 1]\n end\n\n return reversed\n end\n\n return SpawnBag\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"playercards/PlayerCardSpawner\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Amount to shift for the next card (zShift) or next row of cards (xShift)\n-- Note that the table rotation is weird, and the X axis is vertical while the\n-- Z axis is horizontal\nlocal SPREAD_Z_SHIFT = -2.3\nlocal SPREAD_X_SHIFT = -3.66\n\nSpawner = { }\n\n-- Spawns a list of cards at the given position/rotation. This will separate cards by size -\n-- investigator, standard, and mini, spawning them in that order with larger cards on bottom. If\n-- there are different types, the provided callback will be called once for each type as it spawns\n-- either a card or deck.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos tts__Vector table where the cards should be spawned (global)\n---@param rot tts__Vector table for the orientation of the spawned cards (global)\n---@param sort boolean True if this list of cards should be sorted before spawning\n---@param callback? function Callback to be called after the card/deck spawns.\nSpawner.spawnCards = function(cardList, pos, rot, sort, callback)\n if sort then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local miniCards = { }\n local standardCards = { }\n local investigatorCards = { }\n\n for _, card in ipairs(cardList) do\n if card.metadata.type == \"Investigator\" then\n table.insert(investigatorCards, card)\n elseif card.metadata.type == \"Minicard\" then\n -- set proper scale for minicards\n card.data.Transform.scaleX = 0.6\n card.data.Transform.scaleZ = 0.6\n table.insert(miniCards, card)\n else\n table.insert(standardCards, card)\n end\n end\n\n -- Spawn each of the three types individually. Y position accounts for the thickness of the spawned deck\n local position = { x = pos.x, y = pos.y, z = pos.z }\n Spawner.spawn(investigatorCards, position, rot, callback)\n\n position.y = position.y + (#investigatorCards + #standardCards) * 0.07\n Spawner.spawn(standardCards, position, rot, callback)\n\n position.y = position.y + (#standardCards + #miniCards) * 0.07\n Spawner.spawn(miniCards, position, rot, callback)\nend\n\nSpawner.spawnCardSpread = function(cardList, startPos, maxCols, rot, sort, callback)\n if sort then\n table.sort(cardList, Spawner.cardComparator)\n end\n\n local position = { x = startPos.x, y = startPos.y, z = startPos.z }\n -- Special handle the first row if we have less than a full single row, but only if there's a\n -- reasonable max column count. Single-row spreads will send a large value for maxCols\n if maxCols \u003c 100 and #cardList \u003c maxCols then\n position.z = startPos.z + ((maxCols - #cardList) / 2 * SPREAD_Z_SHIFT)\n end\n local cardsInRow = 0\n local rows = 0\n for _, card in ipairs(cardList) do\n Spawner.spawn({ card }, position, rot, callback)\n position.z = position.z + SPREAD_Z_SHIFT\n cardsInRow = cardsInRow + 1\n if cardsInRow \u003e= maxCols then\n rows = rows + 1\n local cardsForRow = #cardList - rows * maxCols\n if cardsForRow \u003e maxCols then\n cardsForRow = maxCols\n end\n position.z = startPos.z + ((maxCols - cardsForRow) / 2 * SPREAD_Z_SHIFT)\n position.x = position.x + SPREAD_X_SHIFT\n cardsInRow = 0\n end\n end\nend\n\n-- Spawn a specific list of cards. This method is for internal use and should not be called\n-- directly, use spawnCards instead.\n---@param cardList table A list of Player Card data structures (data/metadata)\n---@param pos table Position where the cards should be spawned (global)\n---@param rot table Rotation for the orientation of the spawned cards (global)\n---@param callback? function callback to be called after the card/deck spawns.\nSpawner.spawn = function(cardList, pos, rot, callback)\n if #cardList == 0 then return end\n\n -- Spawn a single card directly\n if #cardList == 1 then\n -- handle sideways card\n if cardList[1].data.SidewaysCard then\n rot = { rot.x, rot.y - 90, rot.z }\n end\n return spawnObjectData({\n data = cardList[1].data,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\n end\n\n -- For multiple cards, construct a deck and spawn that\n local deckScaleX = cardList[1].data.Transform.scaleX\n local deckScaleZ = cardList[1].data.Transform.scaleZ\n local deck = Spawner.buildDeckDataTemplate(deckScaleX, deckScaleZ)\n\n local sidewaysDeck = true\n for _, spawnCard in ipairs(cardList) do\n Spawner.addCardToDeck(deck, spawnCard.data)\n -- set sidewaysDeck to false if any card is not a sideways card\n sidewaysDeck = (sidewaysDeck and spawnCard.data.SidewaysCard)\n end\n\n -- set the alt view angle for sideways decks\n if sidewaysDeck then\n deck.AltLookAngle = { x = 0, y = 180, z = 90 }\n rot = { rot.x, rot.y - 90, rot.z }\n end\n\n return spawnObjectData({\n data = deck,\n position = pos,\n rotation = rot,\n callback_function = callback\n })\nend\n\n-- Inserts a card into the given deck. This does three things:\n-- 1. Add the card's data to ContainedObjects\n-- 2. Add the card's ID (the TTS CardID, not the Arkham ID) to the deck's\n-- ID list. Note that the deck's ID list is \"DeckIDs\" even though it\n-- contains a list of card Ids\n-- 3. Extract the card's CustomDeck table and add it to the deck. The deck's\n-- \"CustomDeck\" field is a list of all CustomDecks used by cards within the\n-- deck, keyed by the DeckID and referencing the custom deck table\n---@param deck table TTS deck data structure to add to\n---@param cardData table Data for the card to be inserted\nSpawner.addCardToDeck = function(deck, cardData)\n for customDeckId, customDeckData in pairs(cardData.CustomDeck) do\n if (deck.CustomDeck[customDeckId] == nil) then\n -- CustomDeck not added to deck yet, add it\n deck.CustomDeck[customDeckId] = customDeckData\n elseif (deck.CustomDeck[customDeckId].FaceURL == customDeckData.FaceURL) then\n -- CustomDeck for this card matches the current one for the deck, do nothing\n else\n -- CustomDeck data conflict\n local newDeckId = nil\n for deckId, customDeck in pairs(deck.CustomDeck) do\n if (customDeckData.FaceURL == customDeck.FaceURL) then\n newDeckId = deckId\n end\n end\n if (newDeckId == nil) then\n -- No non-conflicting custom deck for this card, add a new one\n newDeckId = Spawner.findNextAvailableId(deck.CustomDeck, \"1000\")\n deck.CustomDeck[newDeckId] = customDeckData\n end\n -- Update the card with the new CustomDeck info\n cardData.CardID = newDeckId..string.sub(cardData.CardID, 5)\n cardData.CustomDeck[customDeckId] = nil\n cardData.CustomDeck[newDeckId] = customDeckData\n break\n end\n end\n table.insert(deck.ContainedObjects, cardData)\n table.insert(deck.DeckIDs, cardData.CardID)\nend\n\n-- Create an empty deck data table which can have cards added to it. This\n-- creates a new table on each call without using metatables or previous\n-- definitions because we can't be sure that TTS doesn't modify the structure\n---@return table deck Table containing the minimal TTS deck data structure\nSpawner.buildDeckDataTemplate = function(deckScaleX, deckScaleZ)\n local deck = {}\n deck.Name = \"Deck\"\n\n -- Card data. DeckIDs and CustomDeck entries will be built from the cards\n deck.ContainedObjects = {}\n deck.DeckIDs = {}\n deck.CustomDeck = {}\n\n -- Transform is required, Position and Rotation will be overridden by the spawn call so can be omitted here\n -- Decks won't inherently scale to the cards in them. The card list being spawned should be all\n -- the same type/size by this point, so use the first card to set the size\n deck.Transform = {\n scaleX = deckScaleX or 1,\n scaleY = 1,\n scaleZ = deckScaleZ or 1,\n }\n\n return deck\nend\n\n-- Returns the first ID which does not exist in the given table, starting at startId and increasing\n---@param objectTable table keyed by strings which are numbers\n---@param startId string possible ID.\n---@return string id \u003e= startId\nSpawner.findNextAvailableId = function(objectTable, startId)\n local id = startId\n while objectTable[id] ~= nil do\n id = tostring(tonumber(id) + 1)\n end\n return id\nend\n\n-- Get the PBCN (Permanent/Bonded/Customizable/Normal) value from the given metadata.\n---@return number PBCN 1 for Permanent, 2 for Bonded or 4 for Normal. The actual values are\n-- irrelevant as they provide only grouping and the order between them doesn't matter.\nSpawner.getpbcn = function(metadata)\n if metadata.permanent then\n return 1\n elseif metadata.bonded_to ~= nil then\n return 2\n else -- Normal card\n return 3\n end\nend\n\n-- Comparison function used to sort the cards in a deck. Groups bonded or\n-- permanent cards first, then sorts within theose types by name/subname.\n-- Normal cards will sort in standard alphabetical order, while\n-- permanent/bonded/customizable will be in reverse alphabetical order.\n--\n-- Since cards spawn in the order provided by this comparator, with the first\n-- cards ending up at the bottom of a pile, this ordering will spawn in reverse\n-- alphabetical order. This presents the cards in order for non-face-down\n-- areas, and presents them in order when Searching the face-down deck.\nSpawner.cardComparator = function(card1, card2)\n local pbcn1 = Spawner.getpbcn(card1.metadata)\n local pbcn2 = Spawner.getpbcn(card2.metadata)\n if pbcn1 ~= pbcn2 then\n return pbcn1 \u003e pbcn2\n end\n if pbcn1 == 3 then\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003c card2.data.Nickname\n end\n return card1.data.Description \u003c card2.data.Description\n else\n if card1.data.Nickname ~= card2.data.Nickname then\n return card1.data.Nickname \u003e card2.data.Nickname\n end\n return card1.data.Description \u003e card2.data.Description\n end\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"playercards/PlayerCardPanel\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"spawnBagState\":{\"placed\":[],\"placedObjects\":[]}}", "MeasureMovement": false, "Name": "Custom_Tile", @@ -202869,7 +144378,7 @@ "scaleZ": 10 }, "Value": 0, - "XmlUI": "\u003c!-- include playercards/PlayerCardPanel.xml --\u003e\n\u003cPanel\n active=\"false\"\n id=\"helpPanel\"\n position=\"-165 -60 -2\"\n rotation=\"0 0 180\"\n height=\"55\"\n width=\"107\"\n color=\"#00000099\"\u003e\n \u003cText\n id=\"helpText\"\n rectAlignment=\"MiddleCenter\"\n height=\"490\"\n width=\"1000\"\n scale=\"0.1 0.1 1\"\n fontSize=\"66\"\n color=\"#F5F5F5\"\n backgroundColor=\"#FF0000\"\n alignment=\"UpperLeft\"\n horizontalOverflow=\"wrap\"\u003e\n• Select a group to place cards\n• Copy the cards you want for your deck\n• Select a new group to clear the placed cards and see new ones\n• Clear to remove all cards\u003c/Text\u003e\n\u003c/Panel\u003e\n\u003c!-- include playercards/PlayerCardPanel.xml --\u003e" + "XmlUI": "" }, { "AltLookAngle": { @@ -202879,9 +144388,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -202939,9 +144448,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -202999,9 +144508,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -203015,7 +144524,7 @@ "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1767069252728651946/04A700179A71859B828E30D2877D802749B8223C/", "WidthScale": 0 }, - "Description": "See Notebook for details.", + "Description": "After you select 'Enable' from this object's context menu, it will remove tokens that get moved over it.", "DragSelectable": true, "GMNotes": "", "GUID": "0a5a29", @@ -203292,522 +144801,6 @@ "Value": 0, "XmlUI": "" }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "18bd3a", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Tablet", - "Nickname": "Mysterious Chanting - Custom Content Blog", - "Snap": true, - "Sticky": true, - "Tablet": { - "PageURL": "https://mysteriouschanting.wordpress.com/" - }, - "Tooltip": true, - "Transform": { - "posX": 20.752, - "posY": 2.742, - "posZ": -32.557, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "5b268d", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Tablet", - "Nickname": "Suggested Ambient Tracks", - "Snap": true, - "Sticky": true, - "Tablet": { - "PageURL": "https://itswritingitself.wordpress.com/2020/01/28/ahlcg-arkham-horror-the-card-game-the-soundtrack/" - }, - "Tooltip": true, - "Transform": { - "posX": 19.781, - "posY": 2.746, - "posZ": -32.863, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b78dae", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Tablet", - "Nickname": "Rulepop - Quick Rules Reference Site", - "Snap": true, - "Sticky": true, - "Tablet": { - "PageURL": "https://rulepop.com/ahc/#about" - }, - "Tooltip": true, - "Transform": { - "posX": 20.67, - "posY": 2.742, - "posZ": -30.868, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "YouTube", - "DragSelectable": true, - "GMNotes": "", - "GUID": "39ec3d", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Tablet", - "Nickname": "Official Learn to Play Video", - "Snap": true, - "Sticky": true, - "Tablet": { - "PageURL": "https://www.youtube.com/watch?v=zzliu_-xNNQ" - }, - "Tooltip": true, - "Transform": { - "posX": 21.923, - "posY": 2.742, - "posZ": -31.133, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "YouTube", - "DragSelectable": true, - "GMNotes": "", - "GUID": "51cb8d", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Tablet", - "Nickname": "In-depth Arkham Horror TTS tutorial", - "Snap": true, - "Sticky": true, - "Tablet": { - "PageURL": "https://www.youtube.com/watch?v=5sCmJ3e4Uos" - }, - "Tooltip": true, - "Transform": { - "posX": 25.502, - "posY": 2.742, - "posZ": -31.091, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "http://cloud-3.steamusercontent.com/ugc/2037355435564792746/B25EA6E0A5FCE0972F1F61F18948885923F4F137/" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e331fc", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Scarlet Keys - Investigator Expansion", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -33.924, - "posY": 3.42, - "posZ": 11.828, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 3.5, - "scaleY": 1, - "scaleZ": 3.5 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://images-cdn.fantasyflightgames.com/filer_public/3f/6e/3f6e6e3d-ed7e-4f69-94b3-a3900386c617/ahc_decklists_v6.pdf" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "9cd82a", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "Starter Decklists", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 22.955, - "posY": 2.455, - "posZ": -30.337, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 2.5, - "scaleY": 1, - "scaleZ": 2.5 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "http://cloud-3.steamusercontent.com/ugc/1814412497119914295/B60DF4305E8031A9FF9DD38E1CC0BB022A694580/" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "7fc24e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "Official FAQ", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 22.421, - "posY": 2.455, - "posZ": -30.681, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 2.5, - "scaleY": 1, - "scaleZ": 2.5 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://images-cdn.fantasyflightgames.com/filer_public/88/53/88538d11-5274-4b4a-ac8c-e8d758f71132/ahc01_learn_to_play_web.pdf" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "49f237", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "Learn to Play", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 22.241, - "posY": 2.455, - "posZ": -29.527, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 2.5, - "scaleY": 1, - "scaleZ": 2.5 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/762723517668487233/EDDD832597F756BE94833B29B70EE21EDA95C677/", - "MaterialIndex": 3, - "MeshURL": "http://cloud-3.steamusercontent.com/ugc/254843371583107453/E3BD9426DD28A525F93BAF54635A969958E991B2/", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "Official Guides, FAQ, tutorial videos and more!", - "DragSelectable": true, - "GMNotes": "", - "GUID": "fcfa7f", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "Rulebooks, Guides and Tablets", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -66, - "posY": 1.473, - "posZ": 55, - "rotX": 0, - "rotY": 280, - "rotZ": 0, - "scaleX": 4.3, - "scaleY": 4.3, - "scaleZ": 4.3 - }, - "Value": 0, - "XmlUI": "" - }, { "AltLookAngle": { "x": 0, @@ -204373,7 +145366,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/VictoryDisplay\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\n\nlocal pendingCall = false\nlocal missingData = {}\nlocal countedVP = {}\n\nlocal highlightMissing = false\nlocal highlightCounted = false\n\n-- button creation when loading the game\nfunction onLoad()\n -- index 0: VP - \"Display\"\n local buttonParameters = {}\n buttonParameters.label = \"0\"\n buttonParameters.click_function = \"none\"\n buttonParameters.function_owner = self\n buttonParameters.scale = { 0.15, 0.15, 0.15 }\n buttonParameters.width = 0\n buttonParameters.height = 0\n buttonParameters.font_size = 600\n buttonParameters.font_color = { 1, 1, 1 }\n buttonParameters.position = { x = -0.72, y = 0.06, z = -0.69 }\n self.createButton(buttonParameters)\n\n -- index 1: VP - \"Play Area\"\n buttonParameters.position.x = 0.65\n self.createButton(buttonParameters)\n\n -- index 2: VP - \"Total\"\n buttonParameters.position.x = 1.69\n self.createButton(buttonParameters)\n\n -- index 3: highlighting button (missing data)\n buttonParameters.label = \"!\"\n buttonParameters.click_function = \"highlightMissingData\"\n buttonParameters.tooltip = \"Enable highlighting of cards without metadata (VP on these is not counted).\"\n buttonParameters.color = { 1, 0, 0 }\n buttonParameters.width = 700\n buttonParameters.height = 800\n buttonParameters.font_size = 700\n buttonParameters.position = { x = 1.82, y = 0.06, z = -1.32 }\n self.createButton(buttonParameters)\n\n -- index 4: highlighting button (counted VP)\n buttonParameters.label = \"?\"\n buttonParameters.click_function = \"highlightCountedVP\"\n buttonParameters.tooltip = \"Enable highlighting of cards with VP.\"\n buttonParameters.color = { 0, 1, 0 }\n buttonParameters.position.x = 1.5\n self.createButton(buttonParameters)\n\n -- update the display label once\n Wait.time(updateCount, 1)\nend\n\n---------------------------------------------------------\n-- events with descriptions\n---------------------------------------------------------\n\n-- dropping an object on the victory display\nfunction onCollisionEnter()\n startUpdate()\nend\n\n-- removing an object from the victory display\nfunction onCollisionExit()\n startUpdate()\nend\n\n-- picking a clue or location up\nfunction onObjectPickUp(_, obj)\n maybeUpdate(obj)\nend\n\n-- dropping a clue or location\nfunction onObjectDrop(_, obj)\n maybeUpdate(obj, 1)\nend\n\n-- flipping a clue/doom or location\nfunction onObjectRotate(obj, _, flip, _, _, oldFlip)\n if flip == oldFlip then return end\n maybeUpdate(obj, 1, true)\nend\n\n-- destroying a clue or location\nfunction onObjectDestroy(obj)\n maybeUpdate(obj)\nend\n\n---------------------------------------------------------\n-- main functionality\n---------------------------------------------------------\n\nfunction maybeUpdate(obj, delay, flipped)\n -- stop if there is already an update call running\n if pendingCall then return end\n\n -- stop if obj is nil (by e.g. dropping a clue onto another and making a stack)\n if obj == nil then return end\n\n -- only continue for clues / doom tokens or locations\n if obj.hasTag(\"Location\") then\n elseif obj.memo == \"clueDoom\" then\n -- only continue if the clue side is up or a doom token is being flipped\n if obj.is_face_down == true and flipped ~= true then return end\n else\n return\n end\n\n -- only continue if the obj in in the play area\n if not playAreaApi.isInPlayArea(obj) then return end\n\n startUpdate(delay)\nend\n\n-- starts an update\nfunction startUpdate(delay)\n -- stop if there is already an update call running\n if pendingCall then return end\n pendingCall = true\n delay = tonumber(delay) or 0\n Wait.time(updateCount, delay + 0.2)\nend\n\n-- counts the VP in the victory display and request the VP count from the play area\nfunction updateCount()\n missingData = {}\n countedVP = {}\n local victoryPoints = {}\n victoryPoints.display = 0\n victoryPoints.playArea = playAreaApi.countVP()\n\n -- count cards in victory display\n for _, obj in ipairs(searchLib.onObject(self, \"isCardOrDeck\")) do\n -- check metadata for VP\n if obj.type == \"Card\" then\n local VP = getCardVP(obj, JSON.decode(obj.getGMNotes()))\n victoryPoints.display = victoryPoints.display + VP\n if VP \u003e 0 then\n table.insert(countedVP, obj)\n end\n\n -- handling for stacked cards\n elseif obj.type == \"Deck\" then\n local VP = 0\n for _, deepObj in ipairs(obj.getObjects()) do\n local deepVP = getCardVP(obj, JSON.decode(deepObj.gm_notes))\n victoryPoints.display = victoryPoints.display + deepVP\n if deepVP \u003e 0 then\n VP = VP + 1\n end\n end\n if VP \u003e 0 then\n table.insert(countedVP, obj)\n end\n end\n end\n\n -- update the buttons that are used as labels\n self.editButton({ index = 0, label = victoryPoints.display })\n self.editButton({ index = 1, label = victoryPoints.playArea })\n self.editButton({ index = 2, label = victoryPoints.display + victoryPoints.playArea })\n\n -- allow new update calls\n pendingCall = false\nend\n\n-- gets the VP count from the notes\nfunction getCardVP(obj, notes)\n local cardVP\n if notes ~= nil then\n -- enemy, treachery etc.\n cardVP = tonumber(notes.victory)\n\n -- location\n if not cardVP then\n -- check the correct side of the location\n if not obj.is_face_down and notes.locationFront ~= nil then\n cardVP = tonumber(notes.locationFront.victory)\n elseif notes.locationBack ~= nil then\n cardVP = tonumber(notes.locationBack.victory)\n end\n end\n if (cardVP or 0) \u003e 0 then\n table.insert(countedVP, obj)\n end\n else\n table.insert(missingData, obj)\n end\n return cardVP or 0\nend\n\n-- toggles the highlight for objects with missing metadata\nfunction highlightMissingData()\n self.editButton({\n index = 3,\n tooltip = (highlightMissing and \"Enable\" or \"Disable\") .. \" highlighting of cards without metadata (VP on these is not counted).\" })\n for _, obj in pairs(missingData) do\n if obj ~= nil then\n if highlightMissing then\n obj.highlightOff(\"Red\")\n else\n obj.highlightOn(\"Red\")\n end\n end\n end\n playAreaApi.highlightMissingData(highlightMissing)\n highlightMissing = not highlightMissing\nend\n\n-- toggles the highlight for objects that were counted\nfunction highlightCountedVP()\n self.editButton({\n index = 4,\n tooltip = (highlightCounted and \"Enable\" or \"Disable\") .. \" highlighting of cards with VP.\"\n })\n for _, obj in pairs(countedVP) do\n if obj ~= nil then\n if highlightCounted then\n obj.highlightOff(\"Green\")\n else\n obj.highlightOn(\"Green\")\n end\n end\n end\n playAreaApi.highlightCountedVP(highlightCounted)\n highlightCounted = not highlightCounted\nend\n\n-- places the provided card in the first empty spot\nfunction placeCard(card)\n local trash = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n\n -- check snap point states\n local snaps = self.getSnapPoints()\n table.sort(snaps, function(a, b) return a.position.x \u003e b.position.x end)\n table.sort(snaps, function(a, b) return a.position.z \u003c b.position.z end)\n\n -- get first empty slot\n local fullSlots = {}\n local positions = {}\n for i, snap in ipairs(snaps) do\n positions[i] = self.positionToWorld(snap.position)\n local searchResult = searchLib.atPosition(positions[i], \"isCardOrDeck\")\n fullSlots[i] = #searchResult \u003e 0\n end\n\n -- remove tokens from the card\n for _, obj in ipairs(searchLib.onObject(card)) do\n -- don't touch decks / cards\n if obj.type == \"Deck\" or obj.type == \"Card\" then\n -- put chaos tokens back into bag\n elseif tokenChecker.isChaosToken(obj) then\n local chaosBag = chaosBagApi.findChaosBag()\n chaosBag.putObject(obj)\n elseif obj.memo ~= nil and obj.getLock() == false then\n trash.putObject(obj)\n end\n end\n\n -- place the card\n local name = card.getName() or \"Unnamed card\"\n for i = 1, 10 do\n if fullSlots[i] ~= true then\n local rot = { 0, 270, card.getRotation().z }\n card.setPositionSmooth(positions[i], false, true)\n card.setRotation(rot)\n broadcastToAll(\"Victory Display: \" .. name .. \" placed into slot \" .. i .. \".\", \"Green\")\n return\n end\n end\n\n broadcastToAll(\"Victory Display is full! \" .. name .. \" placed into slot 1.\", \"Orange\")\n card.setPositionSmooth(positions[1], false, true)\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/VictoryDisplay\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/token/TokenChecker\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local CHAOS_TOKEN_NAMES = {\n [\"Elder Sign\"] = true,\n [\"+1\"] = true,\n [\"0\"] = true,\n [\"-1\"] = true,\n [\"-2\"] = true,\n [\"-3\"] = true,\n [\"-4\"] = true,\n [\"-5\"] = true,\n [\"-6\"] = true,\n [\"-7\"] = true,\n [\"-8\"] = true,\n [\"Skull\"] = true,\n [\"Cultist\"] = true,\n [\"Tablet\"] = true,\n [\"Elder Thing\"] = true,\n [\"Auto-fail\"] = true,\n [\"Bless\"] = true,\n [\"Curse\"] = true,\n [\"Frost\"] = true\n }\n\n local TokenChecker = {}\n\n -- returns true if the passed object is a chaos token (by name)\n TokenChecker.isChaosToken = function(obj)\n if obj.type == \"Tile\" and CHAOS_TOKEN_NAMES[obj.getName()] then\n return true\n else\n return false\n end\n end\n\n return TokenChecker\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/VictoryDisplay\")\nend)\n__bundle_register(\"core/VictoryDisplay\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckLib = require(\"util/DeckLib\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal tokenChecker = require(\"core/token/TokenChecker\")\n\nlocal pendingCall = false\nlocal missingData = {}\nlocal countedVP = {}\n\nlocal highlightMissing = false\nlocal highlightCounted = false\n\n-- button creation when loading the game\nfunction onLoad()\n -- index 0: VP - \"Display\"\n local buttonParameters = {}\n buttonParameters.label = \"0\"\n buttonParameters.click_function = \"none\"\n buttonParameters.function_owner = self\n buttonParameters.scale = { 0.15, 0.15, 0.15 }\n buttonParameters.width = 0\n buttonParameters.height = 0\n buttonParameters.font_size = 600\n buttonParameters.font_color = { 1, 1, 1 }\n buttonParameters.position = { x = -0.72, y = 0.06, z = -0.69 }\n self.createButton(buttonParameters)\n\n -- index 1: VP - \"Play Area\"\n buttonParameters.position.x = 0.65\n self.createButton(buttonParameters)\n\n -- index 2: VP - \"Total\"\n buttonParameters.position.x = 1.69\n self.createButton(buttonParameters)\n\n -- index 3: highlighting button (missing data)\n buttonParameters.label = \"!\"\n buttonParameters.click_function = \"highlightMissingData\"\n buttonParameters.tooltip = \"Enable highlighting of cards without metadata (VP on these is not counted).\"\n buttonParameters.color = { 1, 0, 0 }\n buttonParameters.width = 700\n buttonParameters.height = 800\n buttonParameters.font_size = 700\n buttonParameters.position = { x = 1.82, y = 0.06, z = -1.32 }\n self.createButton(buttonParameters)\n\n -- index 4: highlighting button (counted VP)\n buttonParameters.label = \"?\"\n buttonParameters.click_function = \"highlightCountedVP\"\n buttonParameters.tooltip = \"Enable highlighting of cards with VP.\"\n buttonParameters.color = { 0, 1, 0 }\n buttonParameters.position.x = 1.5\n self.createButton(buttonParameters)\n\n -- update the display label once\n Wait.time(updateCount, 1)\nend\n\n---------------------------------------------------------\n-- events with descriptions\n---------------------------------------------------------\n\n-- dropping an object on the victory display\nfunction onCollisionEnter()\n startUpdate()\nend\n\n-- removing an object from the victory display\nfunction onCollisionExit()\n startUpdate()\nend\n\n-- picking a clue or location up\nfunction onObjectPickUp(_, obj)\n maybeUpdate(obj)\nend\n\n-- dropping a clue or location\nfunction onObjectDrop(_, obj)\n maybeUpdate(obj, 1)\nend\n\n-- flipping a clue/doom or location\nfunction onObjectRotate(obj, _, flip, _, _, oldFlip)\n if flip == oldFlip then return end\n maybeUpdate(obj, 1, true)\nend\n\n-- destroying a clue or location\nfunction onObjectDestroy(obj)\n maybeUpdate(obj)\nend\n\n---------------------------------------------------------\n-- main functionality\n---------------------------------------------------------\n\nfunction maybeUpdate(obj, delay, flipped)\n -- stop if there is already an update call running\n if pendingCall then return end\n\n -- stop if obj is nil (by e.g. dropping a clue onto another and making a stack)\n if obj == nil then return end\n\n -- only continue for clues / doom tokens or locations\n if obj.hasTag(\"Location\") then\n elseif obj.memo == \"clueDoom\" then\n -- only continue if the clue side is up or a doom token is being flipped\n if obj.is_face_down == true and flipped ~= true then return end\n else\n return\n end\n\n -- only continue if the obj in in the play area\n if not playAreaApi.isInPlayArea(obj) then return end\n\n startUpdate(delay)\nend\n\n-- starts an update\nfunction startUpdate(delay)\n -- stop if there is already an update call running\n if pendingCall then return end\n pendingCall = true\n delay = tonumber(delay) or 0\n Wait.time(updateCount, delay + 0.2)\nend\n\n-- counts the VP in the victory display and request the VP count from the play area\nfunction updateCount()\n missingData = {}\n countedVP = {}\n local victoryPoints = {}\n victoryPoints.display = 0\n victoryPoints.playArea = playAreaApi.countVP()\n\n -- count cards in victory display\n for _, obj in ipairs(searchLib.onObject(self, \"isCardOrDeck\")) do\n -- check metadata for VP\n if obj.type == \"Card\" then\n local VP = getCardVP(obj, JSON.decode(obj.getGMNotes()))\n victoryPoints.display = victoryPoints.display + VP\n if VP \u003e 0 then\n table.insert(countedVP, obj)\n end\n\n -- handling for stacked cards\n elseif obj.type == \"Deck\" then\n local VP = 0\n for _, deepObj in ipairs(obj.getObjects()) do\n local deepVP = getCardVP(obj, JSON.decode(deepObj.gm_notes))\n victoryPoints.display = victoryPoints.display + deepVP\n if deepVP \u003e 0 then\n VP = VP + 1\n end\n end\n if VP \u003e 0 then\n table.insert(countedVP, obj)\n end\n end\n end\n\n -- update the buttons that are used as labels\n self.editButton({ index = 0, label = victoryPoints.display })\n self.editButton({ index = 1, label = victoryPoints.playArea })\n self.editButton({ index = 2, label = victoryPoints.display + victoryPoints.playArea })\n\n -- allow new update calls\n pendingCall = false\nend\n\n-- gets the VP count from the notes\nfunction getCardVP(obj, notes)\n local cardVP\n if notes ~= nil then\n -- enemy, treachery etc.\n cardVP = tonumber(notes.victory)\n\n -- location\n if not cardVP then\n -- check the correct side of the location\n if not obj.is_face_down and notes.locationFront ~= nil then\n cardVP = tonumber(notes.locationFront.victory)\n elseif notes.locationBack ~= nil then\n cardVP = tonumber(notes.locationBack.victory)\n end\n end\n if (cardVP or 0) \u003e 0 then\n table.insert(countedVP, obj)\n end\n else\n table.insert(missingData, obj)\n end\n return cardVP or 0\nend\n\n-- toggles the highlight for objects with missing metadata\nfunction highlightMissingData()\n self.editButton({\n index = 3,\n tooltip = (highlightMissing and \"Enable\" or \"Disable\") .. \" highlighting of cards without metadata (VP on these is not counted).\" })\n for _, obj in pairs(missingData) do\n if obj ~= nil then\n if highlightMissing then\n obj.highlightOff(\"Red\")\n else\n obj.highlightOn(\"Red\")\n end\n end\n end\n playAreaApi.highlightMissingData(highlightMissing)\n highlightMissing = not highlightMissing\nend\n\n-- toggles the highlight for objects that were counted\nfunction highlightCountedVP()\n self.editButton({\n index = 4,\n tooltip = (highlightCounted and \"Enable\" or \"Disable\") .. \" highlighting of cards with VP.\"\n })\n for _, obj in pairs(countedVP) do\n if obj ~= nil then\n if highlightCounted then\n obj.highlightOff(\"Green\")\n else\n obj.highlightOn(\"Green\")\n end\n end\n end\n playAreaApi.highlightCountedVP(highlightCounted)\n highlightCounted = not highlightCounted\nend\n\n-- places the provided card in the first empty spot\nfunction placeCard(card)\n local name = card.getName() or \"Unnamed card\"\n\n -- get sorted list of snap points to check slots\n local snaps = self.getSnapPoints()\n table.sort(snaps, function(a, b) return a.position.x \u003e b.position.x end)\n table.sort(snaps, function(a, b) return a.position.z \u003c b.position.z end)\n\n -- get first empty slot\n local emptyIndex, emptyPos\n for i, snap in ipairs(snaps) do\n local snapPos = self.positionToWorld(snap.position)\n local searchResult = searchLib.atPosition(snapPos, \"isCardOrDeck\")\n if #searchResult == 0 then\n emptyPos = snapPos\n emptyIndex = i\n break\n end\n end\n\n -- remove tokens from the card\n local trash = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n for _, obj in ipairs(searchLib.onObject(card, \"isTileOrToken\")) do\n if tokenChecker.isChaosToken(obj) then\n -- put chaos tokens back into bag\n local chaosBag = chaosBagApi.findChaosBag()\n chaosBag.putObject(obj)\n elseif obj.memo ~= nil and obj.getLock() == false then\n trash.putObject(obj)\n end\n end\n\n -- use the first snap position in case all slots are full\n local pos = emptyPos or self.positionToWorld(snaps[1].position)\n local rot = card.getRotation():setAt(\"x\", 0):setAt(\"y\", 270)\n deckLib.placeOrMergeIntoDeck(card, pos, rot)\n\n -- give a feedback message\n if emptyPos then\n broadcastToAll(\"Victory Display: \" .. name .. \" placed into slot \" .. emptyIndex .. \".\", \"Green\")\n else\n broadcastToAll(\"Victory Display is full! \" .. name .. \" placed into slot 1.\", \"Orange\")\n end\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Token", @@ -204408,9 +145401,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -204465,9 +145458,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -204537,3508 +145530,12 @@ "z": 0 }, "Autoraise": true, - "Bag": { - "Order": 0 - }, "ColorDiffuse": { + "a": 0.27, "b": 1, "g": 1, "r": 1 }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2001%20Night%20of%20the%20Zealot.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "276907", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "Night of the Zealot", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.463, - "posY": 3.037, - "posZ": 36.845, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/01%20Night%20of%20the%20Zealot.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "56a91d", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "01 Night of the Zealot", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": 35, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/51eeefcbe1d1eded152916465d88296faf66528b/Dual%20Pages%2002%20The%20Dunwich%20Legacy.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8df5fc", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Dunwich Legacy", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 67.375, - "posY": 3.038, - "posZ": 30.123, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/02%20Dunwich%20Legacy.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "38d1cd", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "02 The Dunwich Legacy", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": 29, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/51eeefcbe1d1eded152916465d88296faf66528b/Dual%20Pages%2003%20The%20Path%20to%20Carcosa.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "df45c0", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 64.825, - "posY": 3.038, - "posZ": 24.224, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/03%20Path%20to%20Carcosa.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "06a742", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "03 The Path to Carcosa", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": 23, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2004%20The%20Forgotten%20Age.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "20c2ad", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Forgotten Age", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 65.567, - "posY": 3.038, - "posZ": 18.74, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2004%20The%20Forgotten%20Age%20-%20Return%20to.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "908cbf", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Forgotten Age (Return to)", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.292, - "posY": 3.038, - "posZ": 17.746, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/04%20Forgotten%20Age.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d5cd12", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "04 The Forgotten Age", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": 17, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2005%20The%20Circle%20Undone.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "692219", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Circle Undone", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 67.989, - "posY": 3.038, - "posZ": 9.81, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2005%20The%20Circle%20Undone%20-%20Return%20to.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "9e9944", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Circle Undone (Return to)", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 65.795, - "posY": 3.038, - "posZ": 11.853, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/05%20Circle%20Undone.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "20d53c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "05 The Circle Undone", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": 11, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2006%20The%20Dream-Eaters%20-%20A%20-%20The%20Dream-Quest.pdf?raw=true" - }, - "Description": "The Dream-Eaters", - "DragSelectable": true, - "GMNotes": "", - "GUID": "47b9c1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Dream Quest (Campaign A)", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.919, - "posY": 3.038, - "posZ": 4.633, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/06A%20Dream%20Quest.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f03c2d", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "06A The Dream-Quest", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": 5, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2006%20The%20Dream-Eaters%20-%20B%20-%20The%20Web%20of%20Dreams.pdf?raw=true" - }, - "Description": "The Dream-Eaters", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ae792e", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "The Web of Dreams (Campaign B)", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 65.358, - "posY": 3.038, - "posZ": -1.704, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/06B%20Web%20of%20Dreams.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "1bac4d", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "06B The Web of Dreams", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": -1, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2007%20The%20Innsmouth%20Conspiracy%20-%20Play%20Order.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f42179", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "07 The Innsmouth Conspiracy - Play Order", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 65.915, - "posY": 3.038, - "posZ": -7.148, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2007%20The%20Innsmouth%20Conspiracy%20-%20Chronolognical.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c50a3a", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "07 The Innsmouth Conspiracy - Chronological", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.932, - "posY": 3.038, - "posZ": -8.659, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/07%20Innsmouth%20Conspiracy.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f5f3b5", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "07 The Innsmouth Conspiracy", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": -7, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2008%20Edge%20of%20the%20Earth.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "754904", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "08 Edge of the Earth", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.88, - "posY": 3.038, - "posZ": -13.447, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/08%20Edge%20of%20the%20Earth.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e32dc3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "08 Edge of the Earth", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": -13, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2009%20The%20Scarlet%20Keys%202%20Scenarios%20and%20Case%20Files.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "c6e8a0", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "09 The Scarlet Keys: Scenarios \u0026 Case Files", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.593, - "posY": 3.038, - "posZ": -20.295, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2009%20The%20Scarlet%20Keys%201%20Setup%20and%20Dossiers.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "abf457", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "09 The Scarlet Keys: Setup and Dossiers", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.336, - "posY": 3.038, - "posZ": -20.774, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/09%20The%20Scarlet%20Keys.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "11d148", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "09 The Scarlet Keys", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": -19, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%2010%20The%20Feast%20of%20Hemlock%20Vale.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "cecfc9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "10 The Feast of Hemlock Vale: Placeholder", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 66.083, - "posY": 3.036, - "posZ": -28.162, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/10%20The%20Feast%20of%20Hemlock%20Vale.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "Not Released Yet", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2e50cf", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "10 The Feast of Hemlock Vale", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 65, - "posY": 1.249, - "posZ": -25, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S1%2006%20Blob.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6ad284", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S1 06 The Blob that Ate Everything", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 56.811, - "posY": 3.037, - "posZ": 36.984, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S1%2005%20Murder%20Hotel.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b13297", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S1 05 Murder at the Excelsior Hotel", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 57.922, - "posY": 3.038, - "posZ": 37.362, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S1%2004%20Guardians%20of%20the%20Abyss.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "6611a9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S1 04 Guardians of the Abyss", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 59.32, - "posY": 3.037, - "posZ": 38.128, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S1%2003%20Labyrinths.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d014ce", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S1 03 The Labyrinths of Lunacy", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 58.243, - "posY": 3.038, - "posZ": 35.677, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S1%2002%20Carnivale.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "538f32", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S01 02 Carinvale of Horrors", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 58.336, - "posY": 3.038, - "posZ": 37.612, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S1%2001%20Rougarou.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "39bf7c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S1 01 Curse of the Rougarou", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 59.496, - "posY": 3.036, - "posZ": 38.605, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S1%20Stand-Alones.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "00a430", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S1 Stand-Alone Scenarios 2016-2020", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 57.747, - "posY": 3.038, - "posZ": 37.913, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/S1%202016-2020.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e227ad", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "S1 Stand-Alones 2016-2020", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 56.5, - "posY": 1.249, - "posZ": 35, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S2%2010%20Fortune%20and%20Folly.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "e19e46", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S2 10 Fortune and Folly", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 55.414, - "posY": 3.038, - "posZ": 27.01, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S2%2009%20Machinations%20Through%20Time.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f5dbf1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S2 09 Machinations Through Time", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 57.54, - "posY": 3.038, - "posZ": 27.504, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S2%2008%20War%20of%20the%20Outer%20Gods.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "bffa04", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S2 08 War of the Outer Gods", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 58.218, - "posY": 3.038, - "posZ": 27.814, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20S2%2007%20Barkham%20Horror.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "409b50", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "S2 07 Barkham Horror", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 57.284, - "posY": 3.038, - "posZ": 27.325, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/S2%202020-202.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4c47d8", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "S2 Stand-Alones 2020-202?", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 56.5, - "posY": 1.249, - "posZ": 29, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%2005%20Red%20Tide%20Rising.pdf?raw=true" - }, - "Description": "Wendy Adams", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f053b0", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 05 Red Tide Rising", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 56.928, - "posY": 3.038, - "posZ": 10.527, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%2004%20By%20the%20Book.pdf?raw=true" - }, - "Description": "Roland Banks", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a37a83", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 04 By the Book", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 58.036, - "posY": 3.038, - "posZ": 11.817, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%2003%20Bad%20Blood.pdf?raw=true" - }, - "Description": "Agnes Baker", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b5fcf1", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 03 Bad Blood", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 58.344, - "posY": 3.038, - "posZ": 11.89, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%2002%20All%20or%20Nothing.pdf?raw=true" - }, - "Description": "\"Skids\" O'Toole", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8950c7", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 02 All or Nothing", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 56.42, - "posY": 3.038, - "posZ": 12.967, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%2001%20Read%20or%20Die.pdf?raw=true" - }, - "Description": "Daisy Walker", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8994ea", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 01 Read or Die", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 56.895, - "posY": 3.038, - "posZ": 11.163, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%20DC%20Death%20Itself.pdf?raw=true" - }, - "Description": "The Pallid Mask", - "DragSelectable": true, - "GMNotes": "", - "GUID": "443855", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 DC Death Itself", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 57.255, - "posY": 3.038, - "posZ": 11.839, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%20DC%20Then%20It%20Multiplied.pdf?raw=true" - }, - "Description": "Undimensioned and Unseen", - "DragSelectable": true, - "GMNotes": "", - "GUID": "d72c48", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 DC Then it Multiplied", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 56.602, - "posY": 3.038, - "posZ": 10.863, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P1%20Challenge%20Scenarios.pdf?raw=true" - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "409b50", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P1 Challenge Scenarios", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 55.947, - "posY": 3.038, - "posZ": 11.905, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/P1%20Challenge%20Scenarios.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8f7e04", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "P1 Challenge Scenarios", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 56.5, - "posY": 1.249, - "posZ": 11, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P2%2002%20Relics%20of%20the%20Past.pdf?raw=true" - }, - "Description": "Monteray Jack", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8950c7", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P2 02 Relics of the Past", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 55.589, - "posY": 3.038, - "posZ": 3.219, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20P2%2001%20Laid%20to%20Rest.pdf?raw=true" - }, - "Description": "Jim Culver", - "DragSelectable": true, - "GMNotes": "", - "GUID": "8994ea", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "P2 01 Laid to Rest", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 56.648, - "posY": 3.038, - "posZ": 3.995, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/P2%20Challenge%20Scenarios.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "", - "DragSelectable": true, - "GMNotes": "", - "GUID": "dcf492", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "P1 Challenge Scenarios", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 56.5, - "posY": 1.249, - "posZ": 5, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20C1%20Dark%20Matter.pdf?raw=true" - }, - "Description": "Designed by Axolotl", - "DragSelectable": true, - "GMNotes": "", - "GUID": "602e48", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "C1 Dark Matter", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 47.575, - "posY": 3.038, - "posZ": 36.067, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/C1%20Dark%20Matter.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "Designed by Axolotl", - "DragSelectable": true, - "GMNotes": "", - "GUID": "3a08d9", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "C1 Dark Matter", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 48, - "posY": 1.249, - "posZ": 35, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20C2%20Alice%20in%20Wonderland.pdf?raw=true" - }, - "Description": "Designed by Tyler Gotch", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4cf017", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "C2 Alice in Wonderland", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 48.535, - "posY": 3.038, - "posZ": 29.793, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/C2%20Alice%20in%20Wonderland.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "Designed by Tyler Gotch", - "DragSelectable": true, - "GMNotes": "", - "GUID": "ed1d0c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "C2 Alice in Wonderland", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 48, - "posY": 1.249, - "posZ": 29, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "Bag": { - "Order": 0 - }, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "ContainedObjects": [ - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "CustomPDF": { - "PDFPage": 0, - "PDFPageOffset": 0, - "PDFPassword": "", - "PDFUrl": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Dual%20Pages%20C3%20Cyclopean%20Foundations.pdf?raw=true" - }, - "Description": "Designed by Tyler Gotch", - "DragSelectable": true, - "GMNotes": "", - "GUID": "0be2cd", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "function onLoad()\r\n self.createInput({\r\n input_function = \"jumpToPage\",\r\n function_owner = self,\r\n label = \"jump to page\",\r\n alignment = 3,\r\n position = Vector(-1.6,0.1,-2.2),\r\n rotation = Vector(0,0,0),\r\n scale = Vector(0.5,0.5,0.5),\r\n width = 2000,\r\n height = 300,\r\n font_size = 250,\r\n font_color = {0.95,0.95,0.95,0.9},\r\n color = {0.3,0.3,0.3,0.6},\r\n tooltip = \"Type which page you wish to jump to, then click off\",\r\n value = \"\",\r\n validation = 1,\r\n tab = 1,\r\n })\r\nend\r\n\r\nfunction jumpToPage(_, _, inputValue, stillEditing)\r\n if inputValue == \"\" or inputValue == nil then return end -- do nothing if input is empty\r\n \r\n if not stillEditing then -- jump to page if not selecting the textbox anymore\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n elseif string.find(inputValue, \"%\\n\") ~= nil then -- jump to page if enter is pressed\r\n inputValue = inputValue.gsub(inputValue, \"%\\n\", \"\")\r\n jump((tonumber(inputValue) + 2)/2)\r\n return\r\n end\r\n \r\n if (tonumber(inputValue:sub(-1)) == nil) then -- check and remove non numeric character\r\n Wait.time(function()\r\n self.editInput({\r\n index = 0,\r\n value = inputValue:sub(1,-2)\r\n })\r\n end, 0.01)\r\n return\r\n end\r\nend\r\n\r\nfunction jump(page)\r\n self.Book.setPage(page - 1) -- offset since 0 index\r\n Wait.time(function() -- clear page search\r\n self.editInput({\r\n index = 0,\r\n value = \"\",\r\n })\r\n end, 0.01)\r\nend", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Custom_PDF", - "Nickname": "C3 Cyclopean Foundations", - "Snap": true, - "Sticky": true, - "Tags": [ - "CleanUpHelper_ignore" - ], - "Tooltip": true, - "Transform": { - "posX": 48.615, - "posY": 3.038, - "posZ": 22.463, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1.76, - "scaleY": 1, - "scaleZ": 1.76 - }, - "Value": 0, - "XmlUI": "" - } - ], - "CustomMesh": { - "CastShadows": true, - "ColliderURL": "", - "Convex": true, - "CustomShader": { - "FresnelStrength": 0, - "SpecularColor": { - "b": 1, - "g": 1, - "r": 1 - }, - "SpecularIntensity": 0, - "SpecularSharpness": 2 - }, - "DiffuseURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/C3%20Cyclopean%20Foundations.jpg?raw=true", - "MaterialIndex": 3, - "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", - "NormalURL": "", - "TypeIndex": 6 - }, - "Description": "Designed by Tyler Gotch", - "DragSelectable": true, - "GMNotes": "", - "GUID": "f72800", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MaterialIndex": -1, - "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", - "Nickname": "C3 Cyclopean Foundations", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 48, - "posY": 1.249, - "posZ": 23, - "rotX": 0, - "rotY": 270, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "When playing with the Return to Versions of the CYOA guides you will need to use the Return to setup card avaliable above the scenario card to modify the original setup of the game.\r\n\r\nEither version can be used to play a Standard campaign. Howevever, for Return to The Forgotten Age and The Circle Undone you will need the Return to guide.", - "DragSelectable": true, - "GMNotes": "", - "GUID": "2275ed", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "Return to Expansions", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 50, - "posY": 1.569, - "posZ": -26, - "rotX": 0, - "rotY": 90, - "rotZ": 0, - "scaleX": 1.25, - "scaleY": 1.25, - "scaleZ": 1.25 - }, - "Value": 0, - "XmlUI": "" - }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "To Update to the latest version of my campaign guides and their respective covers. On windows go to...\r\nC:\\Users\\[USERNAME]\\Documents\\My Games\\Tabletop Simulator\\Mods\\PDF\r\nAnd search for httpsgithubcomAntimarkovnikov and delete all files find with that in the filename and reload the MOD.", - "DragSelectable": true, - "GMNotes": "", - "GUID": "a1b358", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "Updating to New Versions", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 44, - "posY": 1.569, - "posZ": -26, - "rotX": 0, - "rotY": 90, - "rotZ": 0, - "scaleX": 1.25, - "scaleY": 1.25, - "scaleZ": 1.25 - }, - "Value": 0, - "XmlUI": "" - } - ], "CustomMesh": { "CastShadows": true, "ColliderURL": "", @@ -208057,11 +145554,11 @@ "MaterialIndex": 3, "MeshURL": "https://github.com/Antimarkovnikov/TTS_AHC_CYOA/blob/master/Book%20Model.obj?raw=true", "NormalURL": "", - "TypeIndex": 6 + "TypeIndex": 0 }, - "Description": "Antimarkovnikov", + "Description": "by Antimarkovnikov", "DragSelectable": true, - "GMNotes": "", + "GMNotes": "extras/cyoa_campaign_guides.json", "GUID": "e87ea2", "Grid": true, "GridProjection": false, @@ -208070,12 +145567,10 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- CYOA Campaign Guides by Antimarkovnikov, scripted by Chr1Z\n-- Utility memory bag by Directsun\n-- Version 2.7.0\n-- Fork of Memory Bag 2.0 by MrStump\n\nCONFIG = {\n MEMORY_GROUP = {\n -- This determines how many frames to wait before actually placing objects onto the table when the \"Place\" button is clicked.\n -- This gives the other bags time to recall their objects.\n -- The delay ONLY occurs if other bags have objects out.\n FRAME_DELAY_BEFORE_PLACING_OBJECTS = 30,\n },\n}\n\n--[[ Memory Bag Groups ]] -------------------------------------------------------\n--[[\nUtility Memory Bags may be added to a named group, called a \"memory group\".\nYou can add a bag to a group through the bag's UI: \"Setup\" \u003e \"Group Name\" (to the left of the bag).\nOnly one bag from a group may have it's contents placed on the table at a time.\nWhen \"Place\" is clicked on a bag, the other bags in it's memory group are recalled.\nBy default a memory bag is not in any group. It's memory group is \"nil\".\n--]]\n\nmemoryGroupName = { memoryBag = self }\nfunction memoryGroupName:get()\n return self._name\nend\n\nfunction memoryGroupName:set(newName)\n GlobalMemoryGroups:unregisterBagInGroup(self:get(), self.memoryBag.getGUID())\n GlobalMemoryGroups:registerBagInGroup(newName, self.memoryBag.getGUID())\n\n if newName == \"\" then\n self._name = nil\n else\n self._name = newName\n end\nend\n\n-- Click the \"Recall\" button on all other bags in my memory group.\nfunction recallOtherBagsInMyGroup()\n for _, bag in ipairs(getOtherBagsInMyGroup()) do\n bag.call('buttonClick_recall')\n end\nend\n\n-- Return \"true\" if another bag in my memory group has any objects out on the table.\nfunction anyOtherBagsInMyGroupArePlaced()\n for _, bag in ipairs(getOtherBagsInMyGroup()) do\n local state = bag.call('areAnyOfMyObjectsPlaced')\n if state then return true end\n end\n\n return false\nend\n\n-- Return \"true\" if at least one object from this memory bag is out on the table.\nfunction areAnyOfMyObjectsPlaced()\n for guid, _ in pairs(memoryList) do\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then\n return true\n end\n end\n return false\nend\n\nfunction getOtherBagsInMyGroup()\n local bags = {}\n for bagGuid, _ in pairs(GlobalMemoryGroups:getGroup(memoryGroupName:get())) do\n if bagGuid ~= self.getGUID() then\n bag = getObjectFromGUID(bagGuid)\n -- \"bag\" is nill if it has been deleted since the last time onLoad() was called.\n if bag ~= nil then\n table.insert(bags, bag)\n end\n end\n end\n return bags\nend\n\n--[[\nThis object provides access to a variable stored on the \"Global script\".\nThe variable holds the names \u0026 guids of all memory bag groups.\nThe global variable is a table and holds data like this:\n{\n 'My First Group Name' = {\n '805ebd' = {},\n '35cc21' = {},\n 'fc8886' = {},\n },\n 'My Second Group Name' = {\n 'f50264' = {},\n '5f5f63' = {},\n },\n}\n--]]\nGlobalMemoryGroups = {\n NAME_OF_GLOBAL_VARIABLE = '_GlobalUtilityMemoryBagGroups',\n}\n\n-- Call me inside this script's \"onLoad()\" method!\nfunction GlobalMemoryGroups:onLoad(myGuid)\n -- Create and initialize the global variable if it doesn't already exist:\n if self:_getGroups() == nil then\n self:_setGroups({})\n end\nend\n\n-- Return the GUIDs of all bags in the \"groupName\". The return value is a dictionary that maps [GUID -\u003e empty table].\nfunction GlobalMemoryGroups:getGroup(groupName)\n guids = self:_getGroups()[groupName] or {}\n return guids\nend\n\n-- Registers a bag in a memory group. Creates a new group if one doesn't exist.\nfunction GlobalMemoryGroups:registerBagInGroup(groupName, bagGuid)\n if groupName == nil or groupName == \"\" then\n return\n end\n\n self:_tryCreateNewGroup(groupName)\n local groups = self:_getGroups()\n groups[groupName][bagGuid] = {}\n self:_setGroups(groups)\nend\n\n-- Removes this bag from the memory group.\nfunction GlobalMemoryGroups:unregisterBagInGroup(groupName, bagGuid)\n local groups = self:_getGroups()\n local group = groups[groupName]\n if group ~= nil then\n group[bagGuid] = nil\n self:_setGroups(groups)\n end\nend\n\n-- Return the global variable, which is a table holding all memory group names \u0026 guids.\nfunction GlobalMemoryGroups:_getGroups()\n return Global.getTable(self.NAME_OF_GLOBAL_VARIABLE)\nend\n\n-- Override the global variable (i.e. the entire table).\nfunction GlobalMemoryGroups:_setGroups(newTable)\n Global.setTable(self.NAME_OF_GLOBAL_VARIABLE, newTable)\nend\n\n-- Add a new memory group named \"groupName\" to the global variable, if one doesn't already exist.\nfunction GlobalMemoryGroups:_tryCreateNewGroup(groupName)\n local groups = self:_getGroups()\n if groups[groupName] == nil then\n groups[groupName] = {}\n self:_setGroups(groups)\n end\nend\n\n-- This object controls the \"Group Name\" input text field that is part of the bag's ingame UI.\ngroupNameInput = {\n greyedOutText = \"Group Name\",\n widthPerCharacter = 100,\n padding = 4,\n memoryBag = self,\n}\nfunction groupNameInput:create(optionalStartingValue)\n local effectiveText = optionalStartingValue or self.greyedOutText\n local width = self:computeWidth(effectiveText)\n\n self.memoryBag.createInput({\n label = self.greyedOutText,\n value = optionalStartingValue or nil,\n alignment = 3, -- Center aligned\n input_function = \"groupNameInput_onCharacterTyped\", function_owner = self.memoryBag,\n position = { 2.1, 0.3, 0 }, rotation = { 0, 270, 0 }, width = width, height = 350,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 },\n })\nend\n\nfunction groupNameInput:computeWidth(text)\n return (string.len(text) + self.padding) * self.widthPerCharacter\nend\n\nfunction groupNameInput:updatedWidth(text)\n self.memoryBag.editInput({\n index = 0,\n width = self:computeWidth(text)\n })\nend\n\nfunction groupNameInput:onCharacterTyped(text, stillEditing)\n if stillEditing then\n self:updatedWidth(text)\n else\n if text == \"\" then\n self:updatedWidth(self.greyedOutText)\n end\n end\nend\n\nfunction groupNameInput_onCharacterTyped(memoryBag, playerColor, text, stillEditing)\n groupNameInput:onCharacterTyped(text, stillEditing)\nend\n\nfunction groupNameInput:setGroupNameToInputField()\n local inputFields = self.memoryBag.getInputs()\n if inputFields ~= nil then\n -- Get input field 0, which corresponds to the groupNameInput.\n -- Unfortunately \"self.getInputs()\" doesn't return the inputs in a guaranteed order.\n local nameField = nil\n for _, field in ipairs(inputFields) do\n if field.index == 0 then\n nameField = field\n end\n end\n\n memoryGroupName:set(nameField.value)\n end\nend\n\n--//////////////////////////////////////////////////////////////////////////////\n\n\nfunction updateSave()\n local data_to_save = { [\"ml\"] = memoryList, [\"groupName\"] = memoryGroupName:get() }\n saved_data = JSON.encode(data_to_save)\n self.script_state = saved_data\nend\n\nfunction combineMemoryFromBagsWithin()\n local bagObjList = self.getObjects()\n for _, bagObj in ipairs(bagObjList) do\n local data = bagObj.lua_script_state\n if data ~= nil then\n local j = JSON.decode(data)\n if j ~= nil and j.ml ~= nil then\n for guid, entry in pairs(j.ml) do\n memoryList[guid] = entry\n end\n end\n end\n end\nend\n\nfunction updateMemoryWithMoves()\n memoryList = memoryListBackup\n --get the first transposed object's coordinates\n local obj = getObjectFromGUID(moveGuid or \"\")\n\n -- p1 is where needs to go, p2 is where it was\n local refObjPos = memoryList[moveGuid].pos\n local deltaPos = findOffsetDistance(obj.getPosition(), refObjPos, nil)\n for guid, entry in pairs(memoryList) do\n memoryList[guid].pos.x = entry.pos.x - deltaPos.x\n memoryList[guid].pos.y = entry.pos.y - deltaPos.y\n memoryList[guid].pos.z = entry.pos.z - deltaPos.z\n end\n\n moveList = {}\nend\n\nfunction onload(saved_data)\n GlobalMemoryGroups:onLoad(self.getGUID())\n AllMemoryBagsInScene:add(self.getGUID())\n\n fresh = true\n if saved_data ~= \"\" then\n local loaded_data = JSON.decode(saved_data)\n --Set up information off of loaded_data\n memoryList = loaded_data.ml\n memoryGroupName:set(loaded_data.groupName)\n else\n --Set up information for if there is no saved saved data\n memoryList = {}\n memoryGroupName:set(nil)\n end\n\n moveList = {}\n moveGuid = nil\n\n if next(memoryList) == nil then\n createSetupButton()\n else\n fresh = false\n createMemoryActionButtons()\n end\nend\n\n--Beginning Setup\n\n\n--Make setup button\nfunction createSetupButton()\n self.createButton({\n label = \"Setup\", click_function = \"buttonClick_setup\", function_owner = self,\n position = { 0, 0.3, 4.5 }, rotation = { 0, 0, 0 }, height = 350, width = 800,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\nend\n\n--Triggered by Transpose button\nfunction buttonClick_transpose()\n moveGuid = nil\n broadcastToAll(\"Select one object and move it- all objects will move relative to the new location\", { 0.75, 0.75, 1 })\n memoryListBackup = duplicateTable(memoryList)\n memoryList = {}\n moveList = {}\n self.clearButtons()\n self.clearInputs()\n createButtonsOnAllObjects(true)\n createSetupActionButtons(true)\nend\n\n--Triggered by setup button,\nfunction buttonClick_setup()\n memoryListBackup = duplicateTable(memoryList)\n memoryList = {}\n self.clearButtons()\n self.clearInputs()\n createButtonsOnAllObjects(false)\n createSetupActionButtons(false)\nend\n\nfunction getAllObjectsInMemory()\n local objTable = {}\n local curObj = {}\n\n for guid in pairs(memoryListBackup) do\n curObj = getObjectFromGUID(guid)\n table.insert(objTable, curObj)\n end\n\n return objTable\n -- return getAllObjects()\nend\n\n--Creates selection buttons on objects\nfunction createButtonsOnAllObjects(move)\n buttonIndexMap = {}\n local howManyButtons = 0\n\n local objsToHaveButtons = {}\n if move == true then\n objsToHaveButtons = getAllObjectsInMemory()\n else\n objsToHaveButtons = getAllObjects()\n end\n\n for _, obj in ipairs(objsToHaveButtons) do\n if obj ~= self then\n --On a normal bag, the button positions aren't the same size as the bag.\n globalScaleFactor = 1 / self.getScale().x\n --Super sweet math to set button positions\n local selfPos = self.getPosition()\n local objPos = obj.getPosition()\n local deltaPos = findOffsetDistance(selfPos, objPos, obj)\n local objPos = rotateLocalCoordinates(deltaPos, self)\n objPos.x = -objPos.x * globalScaleFactor\n objPos.y = objPos.y * globalScaleFactor\n objPos.z = objPos.z * globalScaleFactor\n --Workaround for custom PDFs\n if obj.Book then\n objPos.y = objPos.y + 0.5\n end\n --Offset rotation of bag\n local rot = self.getRotation()\n rot.y = -rot.y + 180\n --Create function\n local funcName = \"selectButton_\" .. howManyButtons\n local func = function() buttonClick_selection(obj, move) end\n local color = { 0.75, 0.25, 0.25, 0.6 }\n local colorMove = { 0, 0, 1, 0.6 }\n if move == true then\n color = colorMove\n end\n self.setVar(funcName, func)\n self.createButton({\n click_function = funcName, function_owner = self,\n position = objPos, rotation = rot, height = 1000, width = 1000,\n color = color,\n })\n buttonIndexMap[obj.getGUID()] = howManyButtons\n howManyButtons = howManyButtons + 1\n end\n end\nend\n\n--Creates submit and cancel buttons\nfunction createSetupActionButtons(move)\n self.createButton({\n label = \"Cancel\", click_function = \"buttonClick_cancel\", function_owner = self,\n position = { 0, 0.3, 4.5 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\n\n self.createButton({\n label = \"Submit\", click_function = \"buttonClick_submit\", function_owner = self,\n position = { 0, 0.3, 5.3 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\n\n if move == false then\n self.createButton({\n label = \"Add\", click_function = \"buttonClick_add\", function_owner = self,\n position = { 0, 0.3, 6.1 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 0.25, 1, 0.25 }\n })\n\n self.createButton({\n label = \"Selection\", click_function = \"editDragSelection\", function_owner = self,\n position = { 0, 0.3, -4.5 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\n groupNameInput:create(memoryGroupName:get())\n\n if fresh == false then\n self.createButton({\n label = \"Set New\", click_function = \"buttonClick_setNew\", function_owner = self,\n position = { 0, 0.3, 6.9 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 0.75, 0.75, 1 }\n })\n self.createButton({\n label = \"Remove\", click_function = \"buttonClick_remove\", function_owner = self,\n position = { 0, 0.3, 7.7 }, rotation = { 0, 0, 0 }, height = 350, width = 1100,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 0.25, 0.25 }\n })\n end\n end\n\n self.createButton({\n label = \"Reset\", click_function = \"buttonClick_reset\", function_owner = self,\n position = { 3, 0.3, 0 }, rotation = { 0, 90, 0 }, height = 350, width = 800,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\nend\n\n--During Setup\n\n\n--Checks or unchecks buttons\nfunction buttonClick_selection(obj, move)\n local index = buttonIndexMap[obj.getGUID()]\n local colorMove = { 0, 0, 1, 0.6 }\n local color = { 0, 1, 0, 0.6 }\n\n previousGuid = selectedGuid\n selectedGuid = obj.getGUID()\n\n theList = memoryList\n if move == true then\n theList = moveList\n if previousGuid ~= nil and previousGuid ~= selectedGuid then\n local prevObj = getObjectFromGUID(previousGuid)\n prevObj.highlightOff()\n self.editButton({ index = previousIndex, color = colorMove })\n theList[previousGuid] = nil\n end\n previousIndex = index\n end\n\n if theList[selectedGuid] == nil then\n self.editButton({ index = index, color = color })\n --Adding pos/rot to memory table\n local pos, rot = obj.getPosition(), obj.getRotation()\n --I need to add it like this or it won't save due to indexing issue\n theList[obj.getGUID()] = {\n pos = { x = round(pos.x, 4), y = round(pos.y, 4), z = round(pos.z, 4) },\n rot = { x = round(rot.x, 4), y = round(rot.y, 4), z = round(rot.z, 4) },\n lock = obj.getLock(),\n tint = obj.getColorTint()\n }\n obj.highlightOn({ 0, 1, 0 })\n else\n color = { 0.75, 0.25, 0.25, 0.6 }\n if move == true then\n color = colorMove\n end\n self.editButton({ index = index, color = color })\n theList[obj.getGUID()] = nil\n obj.highlightOff()\n end\nend\n\nfunction editDragSelection(bagObj, player, remove)\n local selectedObjs = Player[player].getSelectedObjects()\n if not remove then\n for _, obj in ipairs(selectedObjs) do\n local index = buttonIndexMap[obj.getGUID()]\n --Ignore if already in the memory list, or does not have a button\n if index and not memoryList[obj.getGUID()] then\n self.editButton({ index = index, color = { 0, 1, 0, 0.6 } })\n --Adding pos/rot to memory table\n local pos, rot = obj.getPosition(), obj.getRotation()\n --I need to add it like this or it won't save due to indexing issue\n memoryList[obj.getGUID()] = {\n pos = { x = round(pos.x, 4), y = round(pos.y, 4), z = round(pos.z, 4) },\n rot = { x = round(rot.x, 4), y = round(rot.y, 4), z = round(rot.z, 4) },\n lock = obj.getLock(),\n tint = obj.getColorTint()\n }\n obj.highlightOn({ 0, 1, 0 })\n end\n end\n else\n for _, obj in ipairs(selectedObjs) do\n local index = buttonIndexMap[obj.getGUID()]\n if index and memoryList[obj.getGUID()] then\n color = { 0.75, 0.25, 0.25, 0.6 }\n self.editButton({ index = index, color = color })\n memoryList[obj.getGUID()] = nil\n obj.highlightOff()\n end\n end\n end\nend\n\n--Cancels selection process\nfunction buttonClick_cancel()\n memoryList = memoryListBackup\n moveList = {}\n self.clearButtons()\n self.clearInputs()\n if next(memoryList) == nil then\n createSetupButton()\n else\n createMemoryActionButtons()\n end\n removeAllHighlights()\n broadcastToAll(\"Selection Canceled\", { 1, 1, 1 })\n moveGuid = nil\nend\n\n--Saves selections\nfunction buttonClick_submit()\n fresh = false\n if next(moveList) ~= nil then\n for guid in pairs(moveList) do\n moveGuid = guid\n end\n if memoryListBackup[moveGuid] == nil then\n broadcastToAll(\"Item selected for moving is not already in memory\", { 1, 0.25, 0.25 })\n else\n broadcastToAll(\"Moving all items in memory relative to new objects position!\", { 0.75, 0.75, 1 })\n self.clearButtons()\n self.clearInputs()\n createMemoryActionButtons()\n local count = 0\n for guid in pairs(moveList) do\n moveGuid = guid\n count = count + 1\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then obj.highlightOff() end\n end\n updateMemoryWithMoves()\n updateSave()\n buttonClick_place()\n end\n elseif next(memoryList) == nil and moveGuid == nil then\n memoryList = memoryListBackup\n broadcastToAll(\"No selections made.\", { 0.75, 0.25, 0.25 })\n end\n combineMemoryFromBagsWithin()\n groupNameInput:setGroupNameToInputField()\n self.clearButtons()\n self.clearInputs()\n createMemoryActionButtons()\n local count = 0\n for guid in pairs(memoryList) do\n count = count + 1\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then obj.highlightOff() end\n end\n broadcastToAll(count .. \" Objects Saved\", { 1, 1, 1 })\n updateSave()\n moveGuid = nil\nend\n\nfunction combineTables(first_table, second_table)\n for k, v in pairs(second_table) do first_table[k] = v end\nend\n\nfunction buttonClick_add()\n fresh = false\n combineTables(memoryList, memoryListBackup)\n broadcastToAll(\"Adding internal bags and selections to existing memory\", { 0.25, 0.75, 0.25 })\n combineMemoryFromBagsWithin()\n self.clearButtons()\n self.clearInputs()\n createMemoryActionButtons()\n local count = 0\n for guid in pairs(memoryList) do\n count = count + 1\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then obj.highlightOff() end\n end\n broadcastToAll(count .. \" Objects Saved\", { 1, 1, 1 })\n updateSave()\nend\n\nfunction buttonClick_remove()\n broadcastToAll(\"Removing Selected Entries From Memory\", { 1.0, 0.25, 0.25 })\n self.clearButtons()\n self.clearInputs()\n createMemoryActionButtons()\n local count = 0\n for guid in pairs(memoryList) do\n count = count + 1\n memoryListBackup[guid] = nil\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then obj.highlightOff() end\n end\n broadcastToAll(count .. \" Objects Removed\", { 1, 1, 1 })\n memoryList = memoryListBackup\n updateSave()\nend\n\nfunction buttonClick_setNew()\n broadcastToAll(\"Setting new position relative to items in memory\", { 0.75, 0.75, 1 })\n self.clearButtons()\n self.clearInputs()\n createMemoryActionButtons()\n local count = 0\n for _, obj in ipairs(getAllObjects()) do\n guid = obj.guid\n if memoryListBackup[guid] ~= nil then\n count = count + 1\n memoryListBackup[guid].pos = obj.getPosition()\n memoryListBackup[guid].rot = obj.getRotation()\n memoryListBackup[guid].lock = obj.getLock()\n memoryListBackup[guid].tint = obj.getColorTint()\n end\n end\n broadcastToAll(count .. \" Objects Saved\", { 1, 1, 1 })\n memoryList = memoryListBackup\n updateSave()\nend\n\n--Resets bag to starting status\nfunction buttonClick_reset()\n fresh = true\n memoryList = {}\n memoryGroupName:set(nil)\n self.clearButtons()\n self.clearInputs()\n createSetupButton()\n removeAllHighlights()\n broadcastToAll(\"Tool Reset\", { 1, 1, 1 })\n updateSave()\nend\n\n--After Setup\n\n\n--Creates recall and place buttons\nfunction createMemoryActionButtons()\n self.createButton({\n label = \"Place\", click_function = \"buttonClick_place\", function_owner = self,\n position = { 0, 0.3, 4.5 }, rotation = { 0, 0, 0 }, height = 350, width = 800,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\n self.createButton({\n label = \"Recall\", click_function = \"buttonClick_recall\", function_owner = self,\n position = { 0, 0.3, 5.3 }, rotation = { 0, 0, 0 }, height = 350, width = 800,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\n self.createButton({\n label = \"Setup\", click_function = \"buttonClick_setup\", function_owner = self,\n position = { 3, 0.3, 0 }, rotation = { 0, 90, 0 }, height = 350, width = 800,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 1, 1, 1 }\n })\n self.createButton({\n label = \"Move\", click_function = \"buttonClick_transpose\", function_owner = self,\n position = { 3.8, 0.3, 0 }, rotation = { 0, 90, 0 }, height = 350, width = 800,\n font_size = 250, color = { 0, 0, 0 }, font_color = { 0.75, 0.75, 1 }\n })\nend\n\n--Sends objects from bag/table to their saved position/rotation\nfunction buttonClick_place()\n if anyOtherBagsInMyGroupArePlaced() then\n recallOtherBagsInMyGroup()\n Wait.frames(_placeObjects, CONFIG.MEMORY_GROUP.FRAME_DELAY_BEFORE_PLACING_OBJECTS)\n else\n _placeObjects()\n end\nend\n\nfunction _placeObjects()\n local bagObjList = self.getObjects()\n for guid, entry in pairs(memoryList) do\n local obj = getObjectFromGUID(guid)\n --If obj is out on the table, move it to the saved pos/rot\n if obj ~= nil then\n obj.setPositionSmooth(entry.pos)\n obj.setRotationSmooth(entry.rot)\n obj.setLock(entry.lock)\n obj.setColorTint(entry.tint)\n else\n --If obj is inside of the bag\n for _, bagObj in ipairs(bagObjList) do\n if bagObj.guid == guid then\n local item = self.takeObject({\n guid = guid, position = entry.pos, rotation = entry.rot, smooth = false\n })\n item.setLock(entry.lock)\n item.setColorTint(entry.tint)\n break\n end\n end\n end\n end\n broadcastToAll(\"Objects Placed\", { 1, 1, 1 })\nend\n\n--Recalls objects to bag from table\nfunction buttonClick_recall()\n for guid, entry in pairs(memoryList) do\n local obj = getObjectFromGUID(guid)\n if obj ~= nil then self.putObject(obj) end\n end\n broadcastToAll(\"Objects Recalled\", { 1, 1, 1 })\nend\n\n--Utility functions\n\n\n--Find delta (difference) between 2 x/y/z coordinates\nfunction findOffsetDistance(p1, p2, obj)\n local yOffset = 0\n if obj ~= nil then\n local bounds = obj.getBounds()\n yOffset = (bounds.size.y - bounds.offset.y)\n end\n local deltaPos = {}\n deltaPos.x = (p2.x - p1.x)\n deltaPos.y = (p2.y - p1.y) + yOffset\n deltaPos.z = (p2.z - p1.z)\n return deltaPos\nend\n\n--Used to rotate a set of coordinates by an angle\nfunction rotateLocalCoordinates(desiredPos, obj)\n local objPos, objRot = obj.getPosition(), obj.getRotation()\n local angle = math.rad(objRot.y)\n local x = desiredPos.x * math.cos(angle) - desiredPos.z * math.sin(angle)\n local z = desiredPos.x * math.sin(angle) + desiredPos.z * math.cos(angle)\n --return {x=objPos.x+x, y=objPos.y+desiredPos.y, z=objPos.z+z}\n return { x = x, y = desiredPos.y, z = z }\nend\n\nfunction rotateMyCoordinates(desiredPos, obj)\n local angle = math.rad(obj.getRotation().y)\n local x = desiredPos.x * math.sin(angle)\n local z = desiredPos.z * math.cos(angle)\n return { x = x, y = desiredPos.y, z = z }\nend\n\n--Coroutine delay, in seconds\nfunction wait(time)\n local start = os.time()\n repeat coroutine.yield(0) until os.time() \u003e start + time\nend\n\n--Duplicates a table (needed to prevent it making reference to the same objects)\nfunction duplicateTable(oldTable)\n local newTable = {}\n for k, v in pairs(oldTable) do\n newTable[k] = v\n end\n return newTable\nend\n\n--Moves scripted highlight from all objects\nfunction removeAllHighlights()\n for _, obj in ipairs(getAllObjects()) do\n obj.highlightOff()\n end\nend\n\n--Round number (num) to the Nth decimal (dec)\nfunction round(num, dec)\n local mult = 10 ^ (dec or 0)\n return math.floor(num * mult + 0.5) / mult\nend\n\n--[[\nThis object provides access to a variable stored on the \"Global script\".\nThe variable holds the GUIDs for every Utility Memory Bag in the scene.\nExample:\n{'805ebd', '35cc21', 'fc8886', 'f50264', '5f5f63'}\n--]]\nAllMemoryBagsInScene = {\n NAME_OF_GLOBAL_VARIABLE = \"_UtilityMemoryBag_AllMemoryBagsInScene\"\n}\n\nfunction AllMemoryBagsInScene:add(guid)\n local guids = Global.getTable(self.NAME_OF_GLOBAL_VARIABLE) or {}\n table.insert(guids, guid)\n Global.setTable(self.NAME_OF_GLOBAL_VARIABLE, guids)\nend\n\nfunction AllMemoryBagsInScene:getGuidList()\n return Global.getTable(self.NAME_OF_GLOBAL_VARIABLE) or {}\nend", - "LuaScriptState": "{\"ml\":{\"06a742\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":23},\"rot\":{\"x\":0,\"y\":269.9891,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"11d148\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":-19},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"1bac4d\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":-1},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"20d53c\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":11},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"2275ed\":{\"lock\":false,\"pos\":{\"x\":50,\"y\":1.569,\"z\":-26},\"rot\":{\"x\":0,\"y\":90.0118,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"2e50cf\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":-25},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"38d1cd\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":29},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"3a08d9\":{\"lock\":false,\"pos\":{\"x\":48,\"y\":1.2494,\"z\":35},\"rot\":{\"x\":0,\"y\":270.0101,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"4c47d8\":{\"lock\":false,\"pos\":{\"x\":56.5,\"y\":1.2494,\"z\":29},\"rot\":{\"x\":0,\"y\":270.0195,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"56a91d\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":35},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"8f7e04\":{\"lock\":false,\"pos\":{\"x\":56.5,\"y\":1.2494,\"z\":11},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"a1b358\":{\"lock\":false,\"pos\":{\"x\":44,\"y\":1.569,\"z\":-26},\"rot\":{\"x\":0,\"y\":89.9871,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"d5cd12\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":17},\"rot\":{\"x\":0,\"y\":270.0119,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"dcf492\":{\"lock\":false,\"pos\":{\"x\":56.5,\"y\":1.2494,\"z\":5},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"e227ad\":{\"lock\":false,\"pos\":{\"x\":56.5,\"y\":1.2494,\"z\":35},\"rot\":{\"x\":0,\"y\":269.9818,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"e32dc3\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":-13},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"ed1d0c\":{\"lock\":false,\"pos\":{\"x\":48,\"y\":1.2494,\"z\":29},\"rot\":{\"x\":0,\"y\":269.9964,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"f03c2d\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":5},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"f5f3b5\":{\"lock\":false,\"pos\":{\"x\":65,\"y\":1.2494,\"z\":-7},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}},\"f72800\":{\"lock\":false,\"pos\":{\"x\":48,\"y\":1.2494,\"z\":23},\"rot\":{\"x\":0,\"y\":270,\"z\":0},\"tint\":{\"a\":1,\"b\":1,\"g\":1,\"r\":1}}}}", - "MaterialIndex": -1, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "", "MeasureMovement": false, - "MeshIndex": -1, - "Name": "Custom_Model_Bag", + "Name": "Custom_Model", "Nickname": "CYOA Campaign Guides", "Snap": true, "Sticky": true, @@ -208143,7 +145638,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/AttachmentHelper\")\nend)\n__bundle_register(\"accessories/AttachmentHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\nlocal fontColor, lastRejectedName\nlocal BACKGROUNDS = {\n {\n title = \"Ancestral Knowledge\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1915746489207287888/2F9F6F211ED0F98E66C9D35D93221E4C7FB6DD3C/\",\n fontcolor = { 1, 1, 1 },\n icons = true\n },\n {\n title = \"Astronomical Atlas\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695853007989004/9153BC204FC707AE564ECFAC063A11CB8C2B5D1E/\",\n fontcolor = { 1, 1, 1 },\n icons = true\n },\n {\n title = \"Backpack\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2018212896278691928/F55BEFFC2540109C6333179532F583B367FF2EBC/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n },\n {\n title = \"Bewitching\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2342503480966345423/F2070B5479C814F35780373966D77D91767A97CC/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Binder's Jar\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228642191/4C149527851C1DBB3015F93DE91667937A3F91DD/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Crystallizer of Dreams\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1915746489207280958/100F16441939E5E23818651D1EB5C209BF3125B9/\",\n fontcolor = { 1, 1, 1 },\n icons = true\n },\n {\n title = \"Diana Stanley\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695635919071208/1AB7222850201630826BFFBA8F2BD0065E2D572F/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Gloria Goldberg\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695635919102502/453D4426118C8A6DE2EA281184716E26CA924C84/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Ikiaq\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228198966/5A408D8D760221DEA164E986B9BE1F79C4803071/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Katja Eastbank\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228203475/62EEE12F4DB1EB80D79B087677459B954380215F/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Ravenous\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228208075/EAC598A450BEE504A7FE179288F1FBBF7ABFA3E0/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n },\n {\n title = \"Sefina Rousseau\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695635919099826/3C3CBFFAADB2ACA9957C736491F470AE906CC953/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n },\n {\n title = \"Stick to the Plan\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2018214163838897493/8E38B96C5A8D703A59009A932432CBE21ABE63A2/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Subject 5U-21\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228199363/CE43D58F37C9F48BDD6E6E145FE29BADEFF4DBC5/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Wooden Sledge\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1750192233783143973/D526236AAE16BDBB98D3F30E27BAFC1D3E21F4AC/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n }\n}\n\n-- save state and options to restore onLoad\nfunction onSave() return JSON.encode({ cardsInBag, showIcons }) end\n\n-- load variables and create context menu\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData)\n cardsInBag = loadedData[1]\n showIcons = loadedData[2]\n fontColor = getFontColor()\n recreateButtons()\n\n self.addContextMenuItem(\"Select image\", selectImage)\n self.addContextMenuItem(\"Toggle skill icons\", function(color)\n showIcons = not showIcons\n printToColor(\"Show skill icons of cards: \" .. tostring(showIcons), color, \"White\")\n refresh()\n end)\nend\n\n-- gets the font color based on background url\nfunction getFontColor()\n local customInfo = self.getCustomObject()\n for i = 1, #BACKGROUNDS do\n if BACKGROUNDS[i].url == customInfo.diffuse then\n return BACKGROUNDS[i].fontcolor\n end\n end\n return { 1, 1, 1 }\nend\n\n-- attempt to load image from below card when dropped\nfunction onDrop(playerColor)\n local pos = self.getPosition():setAt(\"y\", 2)\n local searchResult = searchLib.belowPosition(pos, \"isCard\")\n if #searchResult == 0 then return end\n local syncName = searchResult[1].getName()\n\n -- remove level information from syncName\n syncName = syncName:gsub(\"%s%(%d%)\", \"\")\n\n -- loop through background table\n for _, bgInfo in ipairs(BACKGROUNDS) do\n if bgInfo.title == syncName then\n printToColor(\"Background for '\" .. syncName .. \"' loaded!\", playerColor, \"Green\")\n showIcons = bgInfo.icons\n updateImage(bgInfo.url)\n return\n end\n end\n printToColor(\"Didn't find background for '\" .. syncName .. \"'!\", playerColor, \"Orange\")\nend\n\n-- called by context menu to change background image\nfunction selectImage(color)\n -- generate list of options\n local options = {}\n for i = 1, #BACKGROUNDS do\n options[i] = BACKGROUNDS[i].title\n end\n\n -- prompt user to select option\n Player[color].showOptionsDialog(\"Select image:\", options, 1, function(_, optionIndex)\n showIcons = BACKGROUNDS[optionIndex].icons\n updateImage(BACKGROUNDS[optionIndex].url)\n end)\nend\n\n-- sets background to the provided URL\nfunction updateImage(url)\n self.script_state = JSON.encode({ cardsInBag, showIcons })\n local customInfo = self.getCustomObject()\n customInfo.diffuse = url\n self.setCustomObject(customInfo)\n self.reload()\nend\n\n-- only allow cards to enter, split decks and reject other objects\nfunction tryObjectEnter(object)\n -- block repeated collisions\n if object.getName() == lastRejectedName then return end\n\n if object.type == \"Deck\" then\n local pos = self.getPosition()\n for i = 1, #object.getObjects() do\n local card = object.takeObject({ position = pos + Vector(0, 0.1 * i, 0), smooth = false })\n findCard(card.getGUID(), card.getName(), card.getGMNotes())\n self.putObject(card)\n end\n recreateButtons()\n return false\n elseif object.type ~= \"Card\" then\n broadcastToAll(\"The 'Attachment Helper' only supports cards.\", \"Orange\")\n lastRejectedName = object.getName()\n Wait.time(function() lastRejectedName = nil end, 1)\n return false\n else\n findCard(object.getGUID(), object.getName(), object.getGMNotes())\n recreateButtons()\n return true\n end\nend\n\n-- removes leaving cards from the \"cardInBag\" table\nfunction onObjectLeaveContainer(container, object)\n if container == self then\n local guid = object.getGUID()\n local found = false\n for i, card in ipairs(cardsInBag) do\n if card.guid == guid then\n table.remove(cardsInBag, i)\n found = true\n break\n end\n end\n\n if found ~= true then\n local name = object.getName()\n for i, card in ipairs(cardsInBag) do\n if card.name == name then\n table.remove(cardsInBag, i)\n break\n end\n end\n end\n recreateButtons()\n end\nend\n\n-- refreshes displayed buttons based on contained cards\nfunction refresh()\n cardsInBag = {}\n for _, object in ipairs(self.getObjects()) do\n findCard(object.guid, object.name, object.gm_notes)\n end\n recreateButtons()\nend\n\n-- gets cost and icons for a card\nfunction findCard(guid, name, GMNotes)\n local icons = {}\n local metadata = JSON.decode(GMNotes) or {}\n local buttonLabel = name or \"unnamed\"\n\n if metadata.cost then\n buttonLabel = \"[\" .. metadata.cost .. \"] \" .. buttonLabel\n end\n\n if showIcons then\n if metadata ~= {} then\n icons[1] = metadata.wildIcons\n icons[2] = metadata.willpowerIcons\n icons[3] = metadata.intellectIcons\n icons[4] = metadata.combatIcons\n icons[5] = metadata.agilityIcons\n end\n\n local IconTypes = { \"Wild\", \"Willpower\", \"Intellect\", \"Combat\", \"Agility\" }\n local found = false\n for i = 1, 5 do\n if icons[i] ~= nil and icons[i] ~= \"\" then\n if found == false then\n buttonLabel = buttonLabel .. \"\\n\"\n found = true\n else\n buttonLabel = buttonLabel .. \" \"\n end\n buttonLabel = buttonLabel .. IconTypes[i] .. \": \" .. icons[i]\n end\n end\n end\n table.insert(cardsInBag, { buttonLabel = buttonLabel, hasIcons = (#icons \u003e 0), name = name, guid = guid })\nend\n\n-- recreates buttons with up-to-date labels\nfunction recreateButtons()\n self.clearButtons()\n local verticalPosition = 1.65\n\n -- create buttons for the last 7 cards that entered\n for i = #cardsInBag, 1, -1 do\n if (i + 7) == #cardsInBag then\n printToAll(\"Only displaying buttons for the last 7 cards.\", \"Orange\")\n break\n end\n\n local card = cardsInBag[i]\n\n -- click function\n local funcName = \"removeCard\" .. card.guid\n self.setVar(funcName, function() removeCard(card.guid) end)\n\n -- font size\n local fontSize = 100\n if card.hasIcons or string.len(card.buttonLabel) \u003e 20 then\n fontSize = 75\n end\n\n -- button creation\n self.createButton({\n label = card.buttonLabel,\n click_function = funcName,\n function_owner = self,\n position = { 0, -0.1, verticalPosition },\n height = 200,\n width = 1200,\n font_size = fontSize\n })\n verticalPosition = verticalPosition - 0.485\n end\n\n local countLabel = #cardsInBag\n local fontSize = 250\n if #cardsInBag == 0 then\n countLabel = \"Attachment Helper\"\n fontSize = 150\n end\n\n self.createButton({\n label = tostring(countLabel),\n click_function = \"none\",\n function_owner = self,\n position = { 0, -0.1, -1.7 },\n height = 0,\n width = 0,\n font_size = fontSize,\n font_color = fontColor\n })\nend\n\n-- click-function for buttons to take a card out of the bag\nfunction removeCard(cardGUID)\n self.takeObject({\n guid = cardGUID,\n rotation = self.getRotation(),\n position = self.getPosition() + Vector(0, 0.25, 0),\n callback_function = function(obj) obj.resting = true end\n })\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/AttachmentHelper\")\nend)\n__bundle_register(\"accessories/AttachmentHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\nlocal fontColor, lastRejectedName\nlocal BACKGROUNDS = {\n {\n title = \"Ancestral Knowledge\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1915746489207287888/2F9F6F211ED0F98E66C9D35D93221E4C7FB6DD3C/\",\n fontcolor = { 1, 1, 1 },\n icons = true\n },\n {\n title = \"Astronomical Atlas\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695853007989004/9153BC204FC707AE564ECFAC063A11CB8C2B5D1E/\",\n fontcolor = { 1, 1, 1 },\n icons = true\n },\n {\n title = \"Backpack\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2018212896278691928/F55BEFFC2540109C6333179532F583B367FF2EBC/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n },\n {\n title = \"Bewitching\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2342503480966345423/F2070B5479C814F35780373966D77D91767A97CC/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Binder's Jar\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228642191/4C149527851C1DBB3015F93DE91667937A3F91DD/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Crystallizer of Dreams\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1915746489207280958/100F16441939E5E23818651D1EB5C209BF3125B9/\",\n fontcolor = { 1, 1, 1 },\n icons = true\n },\n {\n title = \"Diana Stanley\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695635919071208/1AB7222850201630826BFFBA8F2BD0065E2D572F/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Gloria Goldberg\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695635919102502/453D4426118C8A6DE2EA281184716E26CA924C84/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Hunting Jacket\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2450601762292308846/0E171E3F3F0D016EEC574F3CA25738A46D95595C/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Ikiaq\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228198966/5A408D8D760221DEA164E986B9BE1F79C4803071/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Katja Eastbank\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228203475/62EEE12F4DB1EB80D79B087677459B954380215F/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Ravenous\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228208075/EAC598A450BEE504A7FE179288F1FBBF7ABFA3E0/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n },\n {\n title = \"Sefina Rousseau\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1754695635919099826/3C3CBFFAADB2ACA9957C736491F470AE906CC953/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n },\n {\n title = \"Stick to the Plan\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2018214163838897493/8E38B96C5A8D703A59009A932432CBE21ABE63A2/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Subject 5U-21\",\n url = \"http://cloud-3.steamusercontent.com/ugc/2021606446228199363/CE43D58F37C9F48BDD6E6E145FE29BADEFF4DBC5/\",\n fontcolor = { 1, 1, 1 },\n icons = false\n },\n {\n title = \"Wooden Sledge\",\n url = \"http://cloud-3.steamusercontent.com/ugc/1750192233783143973/D526236AAE16BDBB98D3F30E27BAFC1D3E21F4AC/\",\n fontcolor = { 0, 0, 0 },\n icons = false\n }\n}\n\n-- save state and options to restore onLoad\nfunction onSave() return JSON.encode({ cardsInBag, showIcons }) end\n\n-- load variables and create context menu\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData)\n cardsInBag = loadedData[1]\n showIcons = loadedData[2]\n fontColor = getFontColor()\n recreateButtons()\n\n self.addContextMenuItem(\"Select image\", selectImage)\n self.addContextMenuItem(\"Toggle skill icons\", function(color)\n showIcons = not showIcons\n printToColor(\"Show skill icons of cards: \" .. tostring(showIcons), color, \"White\")\n refresh()\n end)\n self.addContextMenuItem(\"Draw all cards\", function(color) self.deal(self.getQuantity(), color) end)\nend\n\n-- gets the font color based on background url\nfunction getFontColor()\n local customInfo = self.getCustomObject()\n for i = 1, #BACKGROUNDS do\n if BACKGROUNDS[i].url == customInfo.diffuse then\n return BACKGROUNDS[i].fontcolor\n end\n end\n return { 1, 1, 1 }\nend\n\n-- attempt to load image from below card when dropped\nfunction onDrop(playerColor)\n local pos = self.getPosition():setAt(\"y\", 2)\n local searchResult = searchLib.belowPosition(pos, \"isCard\")\n if #searchResult == 0 then return end\n local syncName = searchResult[1].getName()\n\n -- remove level information from syncName\n syncName = syncName:gsub(\"%s%(%d%)\", \"\")\n\n -- loop through background table\n for _, bgInfo in ipairs(BACKGROUNDS) do\n if bgInfo.title == syncName then\n printToColor(\"Background for '\" .. syncName .. \"' loaded!\", playerColor, \"Green\")\n showIcons = bgInfo.icons\n updateImage(bgInfo.url)\n return\n end\n end\n printToColor(\"Didn't find background for '\" .. syncName .. \"'!\", playerColor, \"Orange\")\nend\n\n-- called by context menu to change background image\nfunction selectImage(color)\n -- generate list of options\n local options = {}\n for i = 1, #BACKGROUNDS do\n options[i] = BACKGROUNDS[i].title\n end\n\n -- prompt user to select option\n Player[color].showOptionsDialog(\"Select image:\", options, 1, function(_, optionIndex)\n showIcons = BACKGROUNDS[optionIndex].icons\n updateImage(BACKGROUNDS[optionIndex].url)\n end)\nend\n\n-- sets background to the provided URL\nfunction updateImage(url)\n self.script_state = JSON.encode({ cardsInBag, showIcons })\n local customInfo = self.getCustomObject()\n customInfo.diffuse = url\n self.setCustomObject(customInfo)\n self.reload()\nend\n\n-- only allow cards to enter, split decks and reject other objects\nfunction tryObjectEnter(object)\n -- block repeated collisions\n if object.getName() == lastRejectedName then return end\n\n if object.type == \"Deck\" then\n local pos = self.getPosition()\n for i = 1, #object.getObjects() do\n local card = object.takeObject({ position = pos + Vector(0, 0.1 * i, 0), smooth = false })\n findCard(card.getGUID(), card.getName(), card.getGMNotes())\n self.putObject(card)\n end\n recreateButtons()\n return false\n elseif object.type ~= \"Card\" then\n broadcastToAll(\"The 'Attachment Helper' only supports cards.\", \"Orange\")\n lastRejectedName = object.getName()\n Wait.time(function() lastRejectedName = nil end, 1)\n return false\n else\n findCard(object.getGUID(), object.getName(), object.getGMNotes())\n recreateButtons()\n return true\n end\nend\n\n-- removes leaving cards from the \"cardInBag\" table\nfunction onObjectLeaveContainer(container, object)\n if container == self then\n local guid = object.getGUID()\n local found = false\n for i, card in ipairs(cardsInBag) do\n if card.guid == guid then\n table.remove(cardsInBag, i)\n found = true\n break\n end\n end\n\n if found ~= true then\n local name = object.getName()\n for i, card in ipairs(cardsInBag) do\n if card.name == name then\n table.remove(cardsInBag, i)\n break\n end\n end\n end\n recreateButtons()\n end\nend\n\n-- refreshes displayed buttons based on contained cards\nfunction refresh()\n cardsInBag = {}\n for _, object in ipairs(self.getObjects()) do\n findCard(object.guid, object.name, object.gm_notes)\n end\n recreateButtons()\nend\n\n-- gets cost and icons for a card\nfunction findCard(guid, name, GMNotes)\n local metadata = JSON.decode(GMNotes) or {}\n local buttonLabel = name or \"unnamed\"\n local hasIcons = false\n\n if metadata.cost then\n buttonLabel = \"[\" .. metadata.cost .. \"] \" .. buttonLabel\n end\n\n if showIcons then\n local iconTypes = { \"Wild\", \"Willpower\", \"Intellect\", \"Combat\", \"Agility\" }\n for _, iconName in ipairs(iconTypes) do\n local mdName = string.lower(iconName) .. \"Icons\"\n if metadata[mdName] ~= nil then\n if hasIcons == false then\n buttonLabel = buttonLabel .. \"\\n\"\n hasIcons = true\n else\n buttonLabel = buttonLabel .. \" \"\n end\n buttonLabel = buttonLabel .. iconName .. \": \" .. metadata[mdName]\n end\n end\n end\n table.insert(cardsInBag, { buttonLabel = buttonLabel, hasIcons = hasIcons, name = name, guid = guid })\nend\n\n-- recreates buttons with up-to-date labels\nfunction recreateButtons()\n self.clearButtons()\n local verticalPosition = 1.65\n\n -- create buttons for the last 7 cards that entered\n for i = #cardsInBag, 1, -1 do\n if (i + 7) == #cardsInBag then\n printToAll(\"Only displaying buttons for the last 7 cards.\", \"Orange\")\n break\n end\n\n local card = cardsInBag[i]\n\n -- click function\n local funcName = \"removeCard\" .. card.guid\n self.setVar(funcName, function() removeCard(card.guid) end)\n\n -- font size\n local fontSize = 100\n if card.hasIcons or string.len(card.buttonLabel) \u003e 20 then\n fontSize = 75\n end\n\n -- button creation\n self.createButton({\n label = card.buttonLabel,\n click_function = funcName,\n function_owner = self,\n position = { 0, -0.1, verticalPosition },\n height = 200,\n width = 1200,\n font_size = fontSize\n })\n verticalPosition = verticalPosition - 0.485\n end\n\n local countLabel = #cardsInBag\n local fontSize = 250\n if #cardsInBag == 0 then\n countLabel = \"Attachment Helper\"\n fontSize = 150\n end\n\n self.createButton({\n label = tostring(countLabel),\n click_function = \"none\",\n function_owner = self,\n position = { 0, -0.1, -1.7 },\n height = 0,\n width = 0,\n font_size = fontSize,\n font_color = fontColor\n })\nend\n\n-- click-function for buttons to take a card out of the bag\nfunction removeCard(cardGUID)\n self.takeObject({\n guid = cardGUID,\n rotation = self.getRotation(),\n position = self.getPosition() + Vector(0, 0.25, 0),\n callback_function = function(obj) obj.resting = true end\n })\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "[[],true]", "MaterialIndex": -1, "MeasureMovement": false, @@ -208215,9 +145710,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -208231,7 +145726,7 @@ "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1838053776205435595/ECFB88938ADBD1EF7AEF713111A11330FD9FAA5A/", "WidthScale": 0 }, - "Description": "Searches the top X cards of the nearest playmat by setting your hand aside and putting the cards into your hand.\n\nPut the target of your search on your set aside hand.", + "Description": "Searches the top X cards of the nearest playermat by setting your hand aside and putting the cards into your hand.\n\nPut the target of your search on your set aside hand.", "DragSelectable": true, "GMNotes": "", "GUID": "17aed0", @@ -208242,7 +145737,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/SearchAssistant\")\nend)\n__bundle_register(\"accessories/SearchAssistant\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal searchLib = require(\"util/SearchLib\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- forward declaration of variables that are used across functions\nlocal matColor, handColor, setAsidePosition, setAsideRotation, drawDeckPosition, topCardDetected\n\nlocal quickParameters = {}\nquickParameters.function_owner = self\nquickParameters.font_size = 165\nquickParameters.width = 275\nquickParameters.height = 275\nquickParameters.color = \"White\"\n\n-- common parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.font_size = 125\nbuttonParameters.width = 650\nbuttonParameters.height = 225\nbuttonParameters.color = \"White\"\n\nlocal inputParameters = {}\ninputParameters.function_owner = self\ninputParameters.input_function = \"updateSearchNumber\"\ninputParameters.tooltip = \"custom search amount\"\ninputParameters.label = \"#\"\ninputParameters.font_size = 175\ninputParameters.width = 400\ninputParameters.height = inputParameters.font_size + 23\ninputParameters.position = { 0, 0.11, 0 }\ninputParameters.alignment = 3\ninputParameters.validation = 2\n\nfunction onLoad()\n normalView()\nend\n\n-- regular view with search box\nfunction normalView()\n self.clearButtons()\n self.clearInputs()\n self.createInput(inputParameters)\n\n -- create custom search button\n buttonParameters.click_function = \"searchCustom\"\n buttonParameters.tooltip = \"Search the entered number of cards\"\n buttonParameters.position = { 0, 0.11, 0.65 }\n buttonParameters.label = \"Search\"\n self.createButton(buttonParameters)\n\n -- create buttons to search 3, 6 or 9 cards\n quickParameters.click_function = \"search3\"\n quickParameters.label = \"3\"\n quickParameters.position = { -0.65, 0.11, -0.65 }\n self.createButton(quickParameters)\n\n quickParameters.click_function = \"search6\"\n quickParameters.label = \"6\"\n quickParameters.position = { 0, 0.11, -0.65 }\n self.createButton(quickParameters)\n\n quickParameters.click_function = \"search9\"\n quickParameters.label = \"9\"\n quickParameters.position = { 0.65, 0.11, -0.65 }\n self.createButton(quickParameters)\nend\n\n-- click functions\nfunction search3(_, playerColor) startSearch(playerColor, 3) end\nfunction search6(_, playerColor) startSearch(playerColor, 6) end\nfunction search9(_, playerColor) startSearch(playerColor, 9) end\n\n-- view during a search with \"done\" buttons\nfunction searchView()\n self.clearButtons()\n self.clearInputs()\n\n -- create the \"End Search\" button\n buttonParameters.click_function = \"endSearch\"\n buttonParameters.tooltip = \"Left-click: Return cards and shuffle\\nRight-click: Return cards without shuffling\"\n buttonParameters.position = { 0, 0.11, 0 }\n buttonParameters.label = \"End Search\"\n self.createButton(buttonParameters)\nend\n\n-- input_function to get number of cards to search\nfunction updateSearchNumber(_, _, input)\n inputParameters.value = tonumber(input)\nend\n\n-- starts the search with the number from the input field\nfunction searchCustom(_, messageColor)\n local number = inputParameters.value\n if number ~= nil then\n startSearch(messageColor, number)\n else\n printToColor(\"Enter the number of cards to search in the textbox.\", messageColor, \"Orange\")\n end\nend\n\n-- start the search (change UI, set handCards aside, draw cards)\nfunction startSearch(messageColor, number)\n matColor = playmatApi.getMatColorByPosition(self.getPosition())\n handColor = playmatApi.getPlayerColor(matColor)\n topCardDetected = false\n\n -- get draw deck\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n if deckAreaObjects.draw == nil then\n printToColor(matColor .. \" draw deck could not be found!\", messageColor, \"Red\")\n return\n end\n\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n drawDeckPosition = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n printToColor(\"Place target(s) of search on set aside hand.\", messageColor, \"Green\")\n\n -- get playmat orientation\n local offset = -15\n if matColor == \"Orange\" or matColor == \"Red\" then\n offset = 15\n end\n\n -- get position and rotation for set aside cards\n local handData = Player[handColor].getHandTransform()\n local handCards = Player[handColor].getHandObjects()\n setAsidePosition = handData.position + offset * handData.right\n setAsideRotation = { handData.rotation.x, handData.rotation.y + 180, 180 }\n\n -- set y-value\n setAsidePosition.y = 1.5\n\n for i = #handCards, 1, -1 do\n handCards[i].setPosition(setAsidePosition + Vector(0, (#handCards - i) * 0.1, 0))\n handCards[i].setRotation(setAsideRotation)\n end\n\n -- handling for Norman Withers\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.flip()\n topCardDetected = true\n end\n\n searchView()\n\n Wait.time(function()\n deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n deckAreaObjects.draw.deal(number, handColor)\n end, 1)\nend\n\n-- place handCards back into deck and optionally shuffle\nfunction endSearch(_, _, isRightClick)\n local handCards = Player[handColor].getHandObjects()\n\n for i = #handCards, 1, -1 do\n handCards[i].setPosition(drawDeckPosition + Vector(0, (#handCards - i) * 0.1, 0))\n handCards[i].setRotation(setAsideRotation)\n end\n\n -- draw set aside cards (from the ground!)\n for _, obj in ipairs(searchLib.atPosition(setAsidePosition, \"isCardOrDeck\")) do\n if obj.type == \"Deck\" then\n Wait.time(function() obj.deal(#obj.getObjects(), handColor) end, 1)\n elseif obj.type == \"Card\" then\n obj.setPosition(Player[handColor].getHandTransform().position)\n obj.flip()\n end\n end\n\n normalView()\n\n -- delay is to wait for cards to enter deck\n if not isRightClick then\n Wait.time(function()\n local deckAreaObjects = playmatApi.getDeckAreaObjects(matColor)\n if deckAreaObjects.draw then\n deckAreaObjects.draw.shuffle()\n end\n end, #handCards * 0.1)\n end\n\n -- Norman Withers handling\n if topCardDetected then\n Wait.time(function() playmatApi.flipTopCardFromDeck(matColor) end, #handCards * 0.1)\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/SearchAssistant\")\nend)\n__bundle_register(\"accessories/SearchAssistant\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal deckLib = require(\"util/DeckLib\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\n\n-- forward declaration of variables that are used across functions\nlocal matColor, handColor, setAsidePosition, setAsideRotation, drawDeckPosition, topCardDetected\nlocal addedVectorLines, addedSnapPoint\n\nlocal quickParameters = {}\nquickParameters.function_owner = self\nquickParameters.font_size = 165\nquickParameters.width = 275\nquickParameters.height = 275\nquickParameters.color = \"White\"\n\n-- common parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.font_size = 125\nbuttonParameters.width = 650\nbuttonParameters.height = 225\nbuttonParameters.color = \"White\"\n\nlocal inputParameters = {}\ninputParameters.function_owner = self\ninputParameters.input_function = \"updateSearchNumber\"\ninputParameters.tooltip = \"custom search amount\"\ninputParameters.label = \"#\"\ninputParameters.font_size = 175\ninputParameters.width = 400\ninputParameters.height = inputParameters.font_size + 23\ninputParameters.position = { 0, 0.11, 0 }\ninputParameters.alignment = 3\ninputParameters.validation = 2\n\nfunction onLoad()\n normalView()\n self.max_typed_number = 9999\nend\n\n-- regular view with search box\nfunction normalView()\n self.clearButtons()\n self.clearInputs()\n self.createInput(inputParameters)\n\n -- create custom search button\n buttonParameters.click_function = \"searchCustom\"\n buttonParameters.tooltip = \"Search the entered number of cards\"\n buttonParameters.position = { 0, 0.11, 0.65 }\n buttonParameters.label = \"Search\"\n self.createButton(buttonParameters)\n\n -- create buttons to search 3, 6 or 9 cards\n quickParameters.click_function = \"search3\"\n quickParameters.label = \"3\"\n quickParameters.position = { -0.65, 0.11, -0.65 }\n self.createButton(quickParameters)\n\n quickParameters.click_function = \"search6\"\n quickParameters.label = \"6\"\n quickParameters.position = { 0, 0.11, -0.65 }\n self.createButton(quickParameters)\n\n quickParameters.click_function = \"search9\"\n quickParameters.label = \"9\"\n quickParameters.position = { 0.65, 0.11, -0.65 }\n self.createButton(quickParameters)\nend\n\n-- click functions\nfunction search3(_, playerColor) startSearch(playerColor, 3) end\nfunction search6(_, playerColor) startSearch(playerColor, 6) end\nfunction search9(_, playerColor) startSearch(playerColor, 9) end\n\n-- view during a search with \"done\" buttons\nfunction searchView()\n self.clearButtons()\n self.clearInputs()\n\n -- create the \"End Search\" button\n buttonParameters.click_function = \"endSearch\"\n buttonParameters.tooltip = \"Left-click: Return cards and shuffle\\nRight-click: Return cards without shuffling\"\n buttonParameters.position = { 0, 0.11, 0 }\n buttonParameters.label = \"End Search\"\n self.createButton(buttonParameters)\nend\n\n-- input_function to get number of cards to search\nfunction updateSearchNumber(_, _, input)\n inputParameters.value = tonumber(input)\nend\n\nfunction onNumberTyped(playerColor, number)\n startSearch(playerColor, number)\nend\n\n-- starts the search with the number from the input field\nfunction searchCustom(_, messageColor)\n local number = inputParameters.value\n if number ~= nil then\n startSearch(messageColor, number)\n else\n printToColor(\"Enter the number of cards to search in the textbox.\", messageColor, \"Orange\")\n end\nend\n\n-- start the search (change UI, set handCards aside, draw cards)\nfunction startSearch(messageColor, number)\n matColor = playermatApi.getMatColorByPosition(self.getPosition())\n handColor = playermatApi.getPlayerColor(matColor)\n topCardDetected = false\n\n -- get draw deck\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n if deckAreaObjects.draw == nil then\n printToColor(matColor .. \" draw deck could not be found!\", messageColor, \"Red\")\n return\n end\n\n -- get bounds to know the height of the deck\n local bounds = deckAreaObjects.draw.getBounds()\n drawDeckPosition = bounds.center + Vector(0, bounds.size.y / 2 + 0.2, 0)\n printToColor(\"Place target(s) of search on set aside spot.\", messageColor, \"Green\")\n\n -- get playermat orientation\n local offset = -15\n if matColor == \"Orange\" or matColor == \"Red\" then\n offset = 15\n end\n\n -- get position and rotation for set aside cards\n local handData = Player[handColor].getHandTransform()\n local handCards = Player[handColor].getHandObjects()\n setAsidePosition = (handData.position + offset * handData.right):setAt(\"y\", 1.5)\n setAsideRotation = Vector(handData.rotation.x, handData.rotation.y + 180, 180)\n\n -- place hand cards set aside\n if #handCards \u003e 0 then\n deckLib.placeOrMergeIntoDeck(handCards, setAsidePosition, setAsideRotation)\n end\n\n -- add a temporary snap point for the set aside spot\n addedSnapPoint = { position = setAsidePosition, rotation = setAsideRotation }\n local snapPoints = Global.getSnapPoints() or {}\n table.insert(snapPoints, addedSnapPoint)\n Global.setSnapPoints(snapPoints)\n\n -- add a temporary box for the set aside spot\n local vectorLines = Global.getVectorLines() or {}\n local boxSize = Vector(2.5, 0, 3.5)\n addedVectorLines = generateBoxData(setAsidePosition, boxSize, setAsideRotation.y, handColor)\n\n for _, line in ipairs(addedVectorLines) do\n table.insert(vectorLines, line)\n end\n Global.setVectorLines(vectorLines)\n\n -- handling for Norman Withers\n if deckAreaObjects.topCard then\n deckAreaObjects.topCard.setRotation(setAsideRotation)\n topCardDetected = true\n end\n\n searchView()\n\n Wait.time(function()\n deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n deckAreaObjects.draw.deal(number, handColor)\n end, 1)\nend\n\n-- place handCards back into deck and optionally shuffle\nfunction endSearch(_, _, isRightClick)\n local handCards = Player[handColor].getHandObjects()\n\n -- place cards on deck\n deckLib.placeOrMergeIntoDeck(handCards, drawDeckPosition, setAsideRotation)\n\n -- draw set aside cards (from the ground!)\n Wait.time(drawSetAsideCards, 0.5 + #handCards * 0.1)\n\n normalView()\n\n Wait.time(function()\n -- maybe shuffle deck\n if not isRightClick then\n local deckAreaObjects = playermatApi.getDeckAreaObjects(matColor)\n if deckAreaObjects.draw then\n deckAreaObjects.draw.shuffle()\n end\n end\n\n -- Norman Withers handling\n if topCardDetected then\n playermatApi.flipTopCardFromDeck(matColor)\n end\n end, 1 + #handCards * 0.1)\nend\n\nfunction drawSetAsideCards()\n for _, obj in ipairs(searchLib.atPosition(setAsidePosition, \"isCardOrDeck\")) do\n local count = 1\n if obj.type == \"Deck\" then\n count = #obj.getObjects()\n end\n obj.deal(count, handColor)\n end\n removeAddedSnapAndLines()\nend\n\nfunction removeAddedSnapAndLines()\n local vectorLines = Global.getVectorLines() or {}\n local snapPoints = Global.getSnapPoints() or {}\n\n -- look for previously added data and remove it (iterate in reverse because we're removing entries)\n for i = #vectorLines, 1, -1 do\n for _, boxLine in ipairs(addedVectorLines) do\n if vectorLines[i].points[1] == boxLine.points[1] and vectorLines[i].points[2] == boxLine.points[2] then\n table.remove(vectorLines, i)\n break\n end\n end\n end\n\n for i = #snapPoints, 1, -1 do\n if snapPoints[i].position == addedSnapPoint.position then\n table.remove(snapPoints, i)\n break\n end\n end\n\n Global.setVectorLines(vectorLines)\n Global.setSnapPoints(snapPoints)\nend\n\n-- generates the lines data for a rectangular box\n---@param center tts__Vector Center of the box\n---@param size tts__Vector X and Z dimension of the box\n---@param rotation number Rotation around the Y-axis for the box\n---@param boxColor string Color for the box\n---@return table lines Vector line data for the box\nfunction generateBoxData(center, size, rotation, boxColor)\n local halfWidth = size.x / 2\n local halfDepth = size.z / 2\n\n -- corners of the box in local coordinates\n local corners = {\n Vector(-halfWidth, 0, -halfDepth),\n Vector(halfWidth, 0, -halfDepth),\n Vector(halfWidth, 0, halfDepth),\n Vector(-halfWidth, 0, halfDepth)\n }\n\n -- translate corners to global coordinates\n for i, cornerVec in ipairs(corners) do\n local rotatedCornerVec = cornerVec:rotateOver('y', rotation)\n corners[i] = rotatedCornerVec + center\n end\n\n -- generate the lines data\n local lines = {\n {\n points = { corners[1], corners[2] },\n color = boxColor\n },\n {\n points = { corners[2], corners[3] },\n color = boxColor\n },\n {\n points = { corners[3], corners[4] },\n color = boxColor\n },\n {\n points = { corners[4], corners[1] },\n color = boxColor\n }\n }\n\n return lines\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/DeckLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckLib = {}\n local searchLib = require(\"util/SearchLib\")\n\n -- places a card/deck at a position or merges into an existing deck below\n ---@param objOrTable tts__Object|table Object or table of objects to move\n ---@param pos table New position for the object\n ---@param rot? table New rotation for the object\n ---@param below? boolean Should the object be placed below an existing deck?\n DeckLib.placeOrMergeIntoDeck = function(objOrTable, pos, rot, below)\n if objOrTable == nil or pos == nil then return end\n\n -- handle 'objOrTable' parameter\n local objects = {}\n if type(objOrTable) == \"table\" then\n objects = objOrTable\n else\n table.insert(objects, objOrTable)\n end\n\n -- search the new position for existing card/deck\n local searchResult = searchLib.atPosition(pos, \"isCardOrDeck\")\n local targetObj\n\n -- get new position\n local offset = 0.5\n local newPos = Vector(pos) + Vector(0, offset, 0)\n\n if #searchResult == 1 then\n targetObj = searchResult[1]\n local bounds = targetObj.getBounds()\n if below then\n newPos = Vector(pos):setAt(\"y\", bounds.center.y - bounds.size.y / 2)\n else\n newPos = Vector(pos):setAt(\"y\", bounds.center.y + bounds.size.y / 2 + offset)\n end\n end\n\n -- process objects in reverse order\n for i = #objects, 1, -1 do\n local obj = objects[i]\n -- add a 0.1 delay for each object (for animation purposes)\n Wait.time(function()\n -- allow moving smoothly out of hand and temporarily lock it\n obj.setLock(true)\n obj.use_hands = false\n\n if rot then\n obj.setRotationSmooth(rot, false, true)\n end\n obj.setPositionSmooth(newPos, false, true)\n\n -- wait for object to finish movement (or 2 seconds)\n Wait.condition(\n function()\n -- revert toggles\n obj.setLock(false)\n obj.use_hands = true\n\n -- use putObject to avoid a TTS bug that merges unrelated cards that are not resting\n if #searchResult == 1 and targetObj ~= obj and not targetObj.isDestroyed() and not obj.isDestroyed() then\n targetObj = targetObj.putObject(obj)\n else\n targetObj = obj\n end\n end,\n -- check state of the object (make sure it's not moving)\n function() return obj.isDestroyed() or not obj.isSmoothMoving() end,\n 2)\n end, (#objects- i) * 0.1)\n end\n end\n\n return DeckLib\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Tile", @@ -208276,9 +145771,9 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0, + "g": 0, + "r": 0 }, "CustomImage": { "CustomTile": { @@ -208292,7 +145787,7 @@ "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1704036721123215146/E44A3B99EACF310E49E94977151A03C9A3DC7F17/", "WidthScale": 0 }, - "Description": "Displays the hand size (total or by title for \"Dream Enhancing Serum\"), hover over it to briefly toggle counting method.\n\nAllows you to randomly discard a card from your hand.", + "Description": "Displays the hand size (total or by title for \"Dream Enhancing Serum\" - hover over it to see the regular count).\n\nAllows you to randomly discard a card from your hand.", "DragSelectable": true, "GMNotes": "", "GUID": "450688", @@ -208303,7 +145798,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"accessories/HandHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- forward declaration of variables that are used across functions\nlocal matColor, handColor, loopId, hovering\n\nfunction onLoad()\n local buttonParamaters = {}\n buttonParamaters.function_owner = self\n\n -- index 0: button as hand size label\n buttonParamaters.hover_color = \"White\"\n buttonParamaters.click_function = \"none\"\n buttonParamaters.position = { 0, 0.11, -0.4 }\n buttonParamaters.height = 0\n buttonParamaters.width = 0\n buttonParamaters.font_size = 500\n buttonParamaters.font_color = \"White\"\n self.createButton(buttonParamaters)\n\n -- index 1: button to toggle \"des\"\n buttonParamaters.label = \"DES: ✗\"\n buttonParamaters.click_function = \"none\"\n buttonParamaters.position = { 0, 0.11, 0.25 }\n buttonParamaters.height = 0\n buttonParamaters.width = 0\n buttonParamaters.font_size = 120\n self.createButton(buttonParamaters)\n\n -- index 2: button to discard a card\n buttonParamaters.label = \"discard random card\"\n buttonParamaters.click_function = \"discardRandom\"\n buttonParamaters.position = { 0, 0.11, 0.7 }\n buttonParamaters.height = 175\n buttonParamaters.width = 900\n buttonParamaters.font_size = 90\n buttonParamaters.font_color = \"Black\"\n self.createButton(buttonParamaters)\n\n updateColors()\n\n -- start loop to update card count\n loopId = Wait.time(updateValue, 1, -1)\nend\n\n-- updates colors when object is dropped somewhere\nfunction onDrop() updateColors() end\n\n-- toggles counting method briefly\nfunction onObjectHover(hover_color, obj)\n -- only continue if correct player hovers over \"self\"\n if obj ~= self or hover_color ~= handColor or hovering then return end\n\n -- toggle this flag so this doesn't get executed multiple times during the delay\n hovering = true\n\n -- stop loop, toggle \"des\" and displayed value briefly, then start new loop after 2s\n Wait.stop(loopId)\n updateValue(true)\n Wait.time(function()\n loopId = Wait.time(updateValue, 1, -1)\n hovering = false\n end, 1)\nend\n\n-- updates the matcolor and handcolor variable\nfunction updateColors()\n matColor = playmatApi.getMatColorByPosition(self.getPosition())\n handColor = playmatApi.getPlayerColor(matColor)\n self.setName(handColor .. \" Hand Helper\")\nend\n\n-- count cards in hand (by name for DES)\nfunction updateValue(toggle)\n -- update colors if handColor doesn't own a handzone\n if Player[handColor].getHandCount() == 0 then\n updateColors()\n end\n\n -- if there is still no handzone, then end here\n if Player[handColor].getHandCount() == 0 then return end\n\n -- get state of \"Dream-Enhancing Serum\" from playermat and update button label\n local des = playmatApi.isDES(matColor)\n if toggle then des = not des end\n self.editButton({ index = 1, label = \"DES: \" .. (des and \"✓\" or \"✗\") })\n\n -- count cards in hand\n local hand = Player[handColor].getHandObjects()\n local size = 0\n\n if des then\n local cardHash = {}\n for _, obj in pairs(hand) do\n if obj.tag == \"Card\" then\n local name = obj.getName()\n local title = string.match(name, '(.+)(%s%(%d+%))') or name\n cardHash[title] = true\n end\n end\n for _, title in pairs(cardHash) do\n size = size + 1\n end\n else\n for _, obj in pairs(hand) do\n if obj.tag == \"Card\" then size = size + 1 end\n end\n end\n\n -- update button label and color\n self.editButton({ index = 0, font_color = des and \"Green\" or \"White\", label = size })\nend\n\n-- discards a random non-hidden card from hand\nfunction discardRandom()\n playmatApi.doDiscardOne(matColor)\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/HandHelper\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/HandHelper\")\nend)\n__bundle_register(\"accessories/HandHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- forward declaration of variables that are used across functions\nlocal matColor, handColor, hovering\n\nfunction onLoad()\n local buttonParamaters = {}\n buttonParamaters.function_owner = self\n\n -- index 0: button as hand size label\n buttonParamaters.hover_color = \"White\"\n buttonParamaters.label = 0\n buttonParamaters.click_function = \"none\"\n buttonParamaters.position = Vector(0, 0.11, -0.4)\n buttonParamaters.height = 0\n buttonParamaters.width = 0\n buttonParamaters.font_size = 500\n buttonParamaters.font_color = \"White\"\n self.createButton(buttonParamaters)\n\n -- index 1: button to toggle \"des\"\n buttonParamaters.label = \"DES: ✗\"\n buttonParamaters.position.z = 0.25\n buttonParamaters.font_size = 120\n self.createButton(buttonParamaters)\n\n -- index 2: button to discard a card\n buttonParamaters.label = \"Discard Random Card\"\n buttonParamaters.click_function = \"discardRandom\"\n buttonParamaters.position.z = 0.7\n buttonParamaters.height = 175\n buttonParamaters.width = 900\n buttonParamaters.font_size = 90\n buttonParamaters.font_color = \"Black\"\n self.createButton(buttonParamaters)\n\n -- make sure this part executes after the playermats are loaded\n Wait.time(function()\n updateColors()\n\n -- start loop to update card count\n playermatApi.checkForDES(matColor)\n Wait.time(updateValue, 1, -1)\n end, 1)\nend\n\n-- updates colors when object is dropped somewhere\nfunction onDrop() updateColors() end\n\n-- disables DES counting while hovered\nfunction onObjectHover(hoverColor, object)\n if hoverColor ~= handColor then return end\n\n if object == self then\n hovering = true\n playermatApi.checkForDES(matColor)\n updateValue()\n else\n hovering = false\n end\nend\n\n-- updates the matcolor and handcolor variable\nfunction updateColors()\n matColor = playermatApi.getMatColorByPosition(self.getPosition())\n handColor = playermatApi.getPlayerColor(matColor)\n self.setName(handColor .. \" Hand Helper\")\nend\n\n-- count cards in hand (by name for DES)\nfunction updateValue()\n -- update colors if handColor doesn't own a handzone\n if Player[handColor].getHandCount() == 0 then\n updateColors()\n end\n\n -- if one of the colors is undefined, then end here\n if matColor == nil or handColor == nil then return end\n\n -- if there is still no handzone, then end here\n if Player[handColor].getHandCount() == 0 then return end\n\n -- get state of \"Dream-Enhancing Serum\" from playermat\n local hasDES = playermatApi.hasDES(matColor)\n\n -- default to regular count if hovered\n if hovering then\n hasDES = false\n end\n\n self.editButton({ index = 1, label = \"DES: \" .. (hasDES and \"✓\" or \"✗\") })\n\n -- count cards in hand\n local hand = Player[handColor].getHandObjects()\n local size = 0\n\n if hasDES then\n local cardHash = {}\n for _, obj in pairs(hand) do\n if obj.type == \"Card\" then\n local name = obj.getName()\n local title = string.match(name, '(.+)(%s%(%d+%))') or name\n cardHash[title] = true\n end\n end\n for _, title in pairs(cardHash) do\n size = size + 1\n end\n else\n for _, obj in pairs(hand) do\n if obj.type == \"Card\" then\n size = size + 1\n end\n end\n end\n\n -- update button label and color\n self.editButton({ index = 0, font_color = hasDES and \"Green\" or \"White\", label = size })\nend\n\n-- discards a random non-hidden card from hand\nfunction discardRandom()\n playermatApi.doDiscardOne(matColor)\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Tile", @@ -208346,14 +145841,14 @@ "MergeDistancePixels": 15, "Stackable": false, "StandUp": false, - "Thickness": 0.2 + "Thickness": 0.1 }, "ImageScalar": 1, "ImageSecondaryURL": "", "ImageURL": "http://cloud-3.steamusercontent.com/ugc/1915746489209870095/5F6A6F2946DBEB81667C15B112F9E35943E61A97/", "WidthScale": 0 }, - "Description": "Moves all objects on the playmat in the chosen direction.", + "Description": "Moves all objects on the playermat in the chosen direction.", "DragSelectable": true, "GMNotes": "", "GUID": "0f1374", @@ -208364,7 +145859,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/DisplacementTool\")\nend)\n__bundle_register(\"accessories/DisplacementTool\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playAreaApi = require(\"core/PlayAreaApi\")\n\nlocal UI_offset = 1.15\n\nlocal buttonParamaters = {}\nbuttonParamaters.function_owner = self\nbuttonParamaters.label = \"\"\nbuttonParamaters.height = 500\nbuttonParamaters.width = 500\nbuttonParamaters.color = { 0, 0, 0, 0 }\n\nfunction onLoad()\n -- index 0: left\n buttonParamaters.click_function = \"shift_left\"\n buttonParamaters.tooltip = \"Move left\"\n buttonParamaters.position = { -UI_offset, 0, 0 }\n self.createButton(buttonParamaters)\n\n -- index 1: right\n buttonParamaters.click_function = \"shift_right\"\n buttonParamaters.tooltip = \"Move right\"\n buttonParamaters.position = { UI_offset, 0, 0 }\n self.createButton(buttonParamaters)\n\n -- index 2: up\n buttonParamaters.click_function = \"shift_up\"\n buttonParamaters.tooltip = \"Move up\"\n buttonParamaters.position = { 0, 0, -UI_offset }\n self.createButton(buttonParamaters)\n\n -- index 3: down\n buttonParamaters.click_function = \"shift_down\"\n buttonParamaters.tooltip = \"Move down\"\n buttonParamaters.position = { 0, 0, UI_offset }\n self.createButton(buttonParamaters)\nend\n\nfunction shift_left(color) playAreaApi.shiftContentsLeft(color) end\n\nfunction shift_right(color) playAreaApi.shiftContentsRight(color) end\n\nfunction shift_up(color) playAreaApi.shiftContentsUp(color) end\n\nfunction shift_down(color) playAreaApi.shiftContentsDown(color) end\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/DisplacementTool\")\nend)\n__bundle_register(\"accessories/DisplacementTool\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playAreaApi = require(\"core/PlayAreaApi\")\n\nlocal UI_offset = 1.15\n\nlocal buttonParamaters = {}\nbuttonParamaters.function_owner = self\nbuttonParamaters.label = \"\"\nbuttonParamaters.height = 500\nbuttonParamaters.width = 500\nbuttonParamaters.color = { 0, 0, 0, 0 }\n\nfunction onLoad()\n -- index 0: left\n buttonParamaters.click_function = \"shift_left\"\n buttonParamaters.tooltip = \"Move left\"\n buttonParamaters.position = { -UI_offset, 0, 0 }\n self.createButton(buttonParamaters)\n\n -- index 1: right\n buttonParamaters.click_function = \"shift_right\"\n buttonParamaters.tooltip = \"Move right\"\n buttonParamaters.position = { UI_offset, 0, 0 }\n self.createButton(buttonParamaters)\n\n -- index 2: up\n buttonParamaters.click_function = \"shift_up\"\n buttonParamaters.tooltip = \"Move up\"\n buttonParamaters.position = { 0, 0, -UI_offset }\n self.createButton(buttonParamaters)\n\n -- index 3: down\n buttonParamaters.click_function = \"shift_down\"\n buttonParamaters.tooltip = \"Move down\"\n buttonParamaters.position = { 0, 0, UI_offset }\n self.createButton(buttonParamaters)\nend\n\nfunction shift_left(color) playAreaApi.shiftContentsLeft(color) end\n\nfunction shift_right(color) playAreaApi.shiftContentsRight(color) end\n\nfunction shift_up(color) playAreaApi.shiftContentsUp(color) end\n\nfunction shift_down(color) playAreaApi.shiftContentsDown(color) end\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Token", @@ -208423,12 +145918,12 @@ { "Name": "option_on", "Type": 0, - "URL": "http://cloud-3.steamusercontent.com/ugc/2024962321889555728/22ABD35CBB49A001F3A5318E4AFCFB22D24FEA39/" + "URL": "http://cloud-3.steamusercontent.com/ugc/2462982115668997008/2178787B67B3C96F3419EDBAB8420E39893756BC/" }, { "Name": "option_off", "Type": 0, - "URL": "http://cloud-3.steamusercontent.com/ugc/2024962321889555661/6643E5CC9160FF4624672C255D0DF7B313DA00A5/" + "URL": "http://cloud-3.steamusercontent.com/ugc/2462982115668996901/D6438ECBB11DECC6DB9987589FF526FBAD4D2368/" } ], "Description": "Cleans up the table for the next scenario during campaign play.\n\nThis includes moving cards and tokens into the trashcans, resetting counters and removing bless/curse tokens from the chaos bag.", @@ -208442,7 +145937,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/CleanUpHelper\")\nend)\n__bundle_register(\"accessories/CleanUpHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Cleans up the table for the next scenario in a campaign:\n-- sets counters to default values (resources and doom) or trauma values (health and sanity, if not disabled) from campaign log\n-- puts everything on playmats and hands into respective trashcans\n-- use the IGNORE_TAG to exclude objects from tidying (default: \"CleanUpHelper_Ignore\")\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal soundCubeApi = require(\"core/SoundCubeApi\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n-- objects with this tag will be ignored\nlocal IGNORE_TAG = \"CleanUpHelper_ignore\"\n\n-- colors and order for following tables\nlocal COLORS = { \"White\", \"Orange\", \"Green\", \"Red\", \"Mythos\" }\nlocal campaignLog\nlocal RESET_VALUES = {}\nlocal loadingFailedBefore = false\nlocal optionsVisible = false\n\nlocal options = {}\noptions[\"importTrauma\"] = true\noptions[\"tidyPlayermats\"] = true\noptions[\"removeDrawnLines\"] = false\n\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\n\n---------------------------------------------------------\n-- option loading and GUI setup\n---------------------------------------------------------\n\nfunction onSave()\n return JSON.encode({ options = options })\nend\n\nfunction onLoad(savedData)\n if savedData ~= nil then\n local loadedData = JSON.decode(savedData)\n options = loadedData.options\n -- update UI to match saved state\n for id, state in pairs(options) do\n self.UI.setAttribute(id, \"image\", state and \"option_on\" or \"option_off\")\n end\n end\n\n -- index 0: button as label\n buttonParameters.label = \"Clean Up Helper\"\n buttonParameters.click_function = \"none\"\n buttonParameters.position = { x = 0, y = 0.1, z = -1.3 }\n buttonParameters.height = 0\n buttonParameters.width = 0\n buttonParameters.font_size = 230\n buttonParameters.font_color = Color(0, 0, 0)\n self.createButton(buttonParameters)\n\n -- index 1: option button\n buttonParameters.label = \"Settings\"\n buttonParameters.click_function = \"showOrHideOptions\"\n buttonParameters.color = { 0, 0, 0, 0.96 }\n buttonParameters.position.z = -0.1\n buttonParameters.height = 350\n buttonParameters.width = 1000\n buttonParameters.font_size = 190\n buttonParameters.font_color = \"White\"\n self.createButton(buttonParameters)\n\n -- index 2: start button\n buttonParameters.label = \"Reset play areas\"\n buttonParameters.click_function = \"cleanUp\"\n buttonParameters.position.z = 1.1\n buttonParameters.width = 1550\n self.createButton(buttonParameters)\nend\n\n---------------------------------------------------------\n-- click functions for option buttons\n---------------------------------------------------------\n\n-- changes the UI state and the internal variable for the togglebuttons\nfunction optionButtonClick(_, id)\n local currentState = options[id]\n local newState = (currentState and \"option_off\" or \"option_on\")\n options[id] = not currentState\n self.UI.setAttribute(id, \"image\", newState)\nend\n\n-- shows or hides the option panel\nfunction showOrHideOptions()\n optionsVisible = not optionsVisible\n\n if optionsVisible then\n self.UI.show(\"options\")\n else\n self.UI.hide(\"options\")\n end\nend\n\n---------------------------------------------------------\n-- main function\n---------------------------------------------------------\n\nfunction cleanUp(_, color)\n printToAll(\"------------------------------\", \"White\")\n printToAll(\"Clean up started!\", \"Orange\")\n printToAll(\"Resetting counters...\", \"White\")\n\n soundCubeApi.playSoundByName(\"Vacuum\")\n ignoreCustomDataHelper()\n getTrauma()\n\n -- delay to account for potential state change of campaign log\n Wait.time(updateCounters, 0.2)\n\n resetDoomCounter()\n blessCurseManagerApi.removeAll(color)\n removeLines()\n discardHands()\n chaosBagApi.returnChaosTokens()\n chaosBagApi.releaseAllSealedTokens(color)\n maybeIgnoreTekeliliCards()\n\n printToAll(\"Tidying main play area...\", \"White\")\n startLuaCoroutine(self, \"tidyPlayareaCoroutine\")\nend\n\n---------------------------------------------------------\n-- modular functions, called by other functions\n---------------------------------------------------------\n\nfunction updateCounters()\n playmatApi.updateCounter(\"All\", \"ResourceCounter\", 5)\n playmatApi.updateCounter(\"All\", \"ClickableClueCounter\", 0)\n playmatApi.resetSkillTracker(\"All\")\n\n for i = 1, 4 do\n playmatApi.updateCounter(COLORS[i], \"DamageCounter\", RESET_VALUES.Damage[i])\n playmatApi.updateCounter(COLORS[i], \"HorrorCounter\", RESET_VALUES.Horror[i])\n end\nend\n\n-- reset doom on agenda\nfunction resetDoomCounter()\n local doomCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomCounter\")\n if doomCounter ~= nil then\n doomCounter.call(\"updateVal\")\n else\n printToAll(\"Doom counter could not be found.\", \"Yellow\")\n end\nend\n\n-- adds the ignore tag to the custom data helper\nfunction ignoreCustomDataHelper()\n local customDataHelper = playAreaApi.getCustomDataHelper()\n if customDataHelper then\n customDataHelper.addTag(IGNORE_TAG)\n end\nend\n\n-- read values for trauma from campaign log if enabled\nfunction getTrauma()\n RESET_VALUES = {\n Damage = { 0, 0, 0, 0 },\n Horror = { 0, 0, 0, 0 }\n }\n\n -- stop here if trauma import is disabled\n if not options[\"importTrauma\"] then\n printToAll(\"Default values for health and sanity loaded.\", \"Yellow\")\n return\n end\n\n -- get campaign log\n campaignLog = getObjectsWithTag(\"CampaignLog\")[1]\n if campaignLog == nil then\n printToAll(\"Campaign log not found in standard position!\", \"Yellow\")\n printToAll(\"Default values for health and sanity loaded.\", \"Yellow\")\n return\n end\n loadTrauma()\nend\n\n-- gets data from campaign log if possible\nfunction loadTrauma()\n -- check if \"returnTrauma\" function exists to avoid calling nil\n local trauma = campaignLog.getVar(\"returnTrauma\")\n\n if trauma ~= nil then\n printToAll(\"Trauma values found in campaign log!\", \"Green\")\n trauma = campaignLog.call(\"returnTrauma\")\n for i = 1, 8 do\n if i \u003c 5 then\n RESET_VALUES.Damage[i] = trauma[i]\n else\n RESET_VALUES.Horror[i - 4] = trauma[i]\n end\n end\n loadingFailedBefore = false\n elseif loadingFailedBefore then\n printToAll(\"Trauma values could not be found in campaign log!\", \"Yellow\")\n printToAll(\"Default values for health and sanity loaded.\", \"Yellow\")\n loadingFailedBefore = false\n else\n -- set campaign log to first state\n local stateId = campaignLog.getStateId()\n\n if stateId ~= 1 then\n campaignLog = campaignLog.setState(1)\n end\n loadingFailedBefore = true\n\n -- small delay to account for potential state change\n Wait.time(loadTrauma, 0.1)\n end\nend\n\n-- remove drawn lines\nfunction removeLines()\n if options[\"removeDrawnLines\"] then\n printToAll(\"Removing global vector lines...\", \"White\")\n Global.setVectorLines({})\n end\nend\n\n-- discard all hand objects\nfunction discardHands()\n if not options[\"tidyPlayermats\"] then return end\n for i = 1, 4 do\n local trash = guidReferenceApi.getObjectByOwnerAndType(COLORS[i], \"Trash\")\n if trash == nil then return end\n local hand = Player[playmatApi.getPlayerColor(COLORS[i])].getHandObjects()\n for j = #hand, 1, -1 do\n trash.putObject(hand[j])\n end\n end\nend\n\n-- maybe ignore cards / decks on the tekelili helper\nfunction maybeIgnoreTekeliliCards()\n local tekeliliHelper = getTekeliliHelper()\n\n if tekeliliHelper then\n local searchResult = searchLib.onObject(tekeliliHelper, \"isCardOrDeck\")\n for _, obj in ipairs(searchResult) do\n obj.addTag(IGNORE_TAG)\n end\n end\nend\n\n-- clean up for play area\nfunction tidyPlayareaCoroutine()\n local trash = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n local playAreaZone = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaZone\")\n\n -- reset the playarea image by not providing an image\n playAreaApi.updateSurface()\n\n if playAreaZone == nil then\n printToAll(\"Scripting zone for main play area could not be found!\", \"Red\")\n elseif trash == nil then\n printToAll(\"Trashcan for main play area could not be found!\", \"Red\")\n else\n for _, obj in ipairs(playAreaZone.getObjects()) do\n -- ignore these elements\n if obj.hasTag(IGNORE_TAG) == false\n and obj.locked == false\n and obj.interactable == true then\n coroutine.yield(0)\n trash.putObject(obj)\n end\n end\n end\n\n printToAll(\"Tidying playermats and mythos area...\", \"White\")\n startLuaCoroutine(self, \"tidyPlayerMatCoroutine\")\n return 1\nend\n\n-- clean up for the four playermats and the mythos area\nfunction tidyPlayerMatCoroutine()\n local tekeliliHelper = getTekeliliHelper()\n\n for i = 1, 5 do\n -- only continue for playermat (1-4) if option enabled\n if options[\"tidyPlayermats\"] or i == 5 then\n -- delay for animation purpose\n for k = 1, 30 do\n coroutine.yield(0)\n end\n\n -- get respective trash\n local trash = guidReferenceApi.getObjectByOwnerAndType(COLORS[i], \"Trash\")\n if trash == nil then\n printToAll(\"Trashcan for \" .. COLORS[i] .. \" playmat could not be found! Skipping this playermat.\", \"Yellow\")\n goto continue\n end\n\n -- maybe store tekelili cards\n if tekeliliHelper and i ~= 5 then\n tekeliliHelper.call(\"storeTekelili\", COLORS[i])\n end\n\n local objList\n if i \u003c 5 then\n objList = playmatApi.searchAroundPlaymat(COLORS[i])\n else\n -- Victory Display + Mythos Area\n objList = searchLib.inArea({ -2, 2, 10 }, { 0, 270, 0 }, { 55, 1, 13.5 })\n end\n\n for _, obj in ipairs(objList) do\n -- ignore these elements\n if obj.hasTag(IGNORE_TAG) == false\n and obj.getDescription() ~= \"Action Token\"\n and obj.hasTag(\"chaosBag\") == false\n and (obj.locked == false or obj.hasTag(\"Investigator\"))\n and obj.interactable == true then\n trash.putObject(obj)\n\n -- action token handling\n elseif obj.getDescription() == \"Action Token\" then\n -- move the small action token to the proper position\n if obj.getScale().x \u003c 0.4 then\n local pos = playmatApi.transformLocalPosition(Vector(-0.865, 0.1, -0.28), COLORS[i])\n obj.setPosition(pos)\n end\n\n -- flip action tokens back to ready\n if obj.is_face_down then\n local rot = playmatApi.returnRotation(COLORS[i])\n obj.setRotation(rot)\n end\n\n -- reset action token state\n local stateId = obj.getStateId()\n if stateId ~= -1 and stateId ~= 6 then\n coroutine.yield(0)\n obj.setState(6)\n end\n end\n end\n\n -- maybe respawn tekelili cards\n if tekeliliHelper and i ~= 5 then\n tekeliliHelper.call(\"spawnStoredTekelili\", COLORS[i])\n end\n end\n ::continue::\n end\n\n -- maybe remove ignore tag from cards / decks on the tekelili helper\n if tekeliliHelper then\n local searchResult = searchLib.onObject(tekeliliHelper, \"isCardOrDeck\")\n for _, obj in ipairs(searchResult) do\n obj.removeTag(IGNORE_TAG)\n end\n end\n\n -- reset \"activeInvestigatorId\"\n local playerMats = guidReferenceApi.getObjectsByType(\"Playermat\")\n for _, mat in pairs(playerMats) do\n mat.setVar(\"activeInvestigatorId\", \"00000\")\n end\n\n -- reset spawned data\n tokenSpawnTrackerApi.resetAll()\n local datahelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n if datahelper then\n datahelper.setTable(\"SPAWNED_PLAYER_CARD_GUIDS\", {})\n end\n\n printToAll(\"Clean up completed!\", \"Green\")\n return 1\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/SoundCubeApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SoundCubeApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- this table links the name of a trigger effect to its index\n local soundIndices = {\n [\"Vacuum\"] = 0,\n [\"Deep Bell\"] = 1,\n [\"Dark Souls\"] = 2\n }\n\n ---@param index number Index of the sound effect to play\n local function playTriggerEffect(index)\n local SoundCube = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"SoundCube\")\n SoundCube.AssetBundle.playTriggerEffect(index)\n end\n\n -- plays the by name requested sound\n ---@param soundName string Name of the sound to play\n SoundCubeApi.playSoundByName = function(soundName)\n playTriggerEffect(soundIndices[soundName])\n end\n\n return SoundCubeApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"resetTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/CleanUpHelper\")\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/token/TokenSpawnTrackerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local TokenSpawnTracker = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getSpawnTracker()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TokenSpawnTracker\")\n end\n\n TokenSpawnTracker.hasSpawnedTokens = function(cardGuid)\n return getSpawnTracker().call(\"hasSpawnedTokens\", cardGuid)\n end\n\n TokenSpawnTracker.markTokensSpawned = function(cardGuid)\n return getSpawnTracker().call(\"markTokensSpawned\", cardGuid)\n end\n\n TokenSpawnTracker.resetTokensSpawned = function(card)\n return getSpawnTracker().call(\"resetTokensSpawned\", card)\n end\n\n TokenSpawnTracker.resetAllAssetAndEvents = function()\n return getSpawnTracker().call(\"resetAllAssetAndEvents\")\n end\n\n TokenSpawnTracker.resetAllLocations = function()\n return getSpawnTracker().call(\"resetAllLocations\")\n end\n\n TokenSpawnTracker.resetAll = function()\n return getSpawnTracker().call(\"resetAll\")\n end\n\n return TokenSpawnTracker\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"accessories/CleanUpHelper\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- Cleans up the table for the next scenario in a campaign:\n-- sets counters to default values (resources and doom) or trauma values (health and sanity, if not disabled) from campaign log\n-- puts everything on playermats and hands into respective trashcans\n-- use the IGNORE_TAG to exclude objects from tidying (default: \"CleanUpHelper_Ignore\")\n\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal soundCubeApi = require(\"core/SoundCubeApi\")\nlocal tokenSpawnTrackerApi = require(\"core/token/TokenSpawnTrackerApi\")\n\n-- objects with this tag will be ignored\nlocal IGNORE_TAG = \"CleanUpHelper_ignore\"\n\n-- colors and order for following tables\nlocal COLORS = { \"White\", \"Orange\", \"Green\", \"Red\" }\nlocal campaignLog\nlocal RESET_VALUES = {}\nlocal loadingFailedBefore = false\nlocal optionsVisible = false\nlocal options = {\n [\"importTrauma\"] = true,\n [\"tidyPlayermats\"] = true,\n [\"removeDrawnLines\"] = false\n}\nlocal removeIgnoreLater = {}\n\n-- don't clean playermats for preludes\nlocal scenarioName\nlocal preludeList = {\n [\"Prelude: Welcome to Hemlock Vale!\"] = true\n}\n\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\n\n---------------------------------------------------------\n-- option loading and GUI setup\n---------------------------------------------------------\n\nfunction onSave()\n return JSON.encode({ options = options })\nend\n\nfunction onLoad(savedData)\n if savedData ~= nil then\n local loadedData = JSON.decode(savedData)\n options = loadedData.options\n -- update UI to match saved state\n for id, state in pairs(options) do\n self.UI.setAttribute(id, \"image\", state and \"option_on\" or \"option_off\")\n end\n end\n\n -- index 0: button as label\n buttonParameters.label = \"Clean Up Helper\"\n buttonParameters.click_function = \"none\"\n buttonParameters.position = { x = 0, y = 0.1, z = -1.3 }\n buttonParameters.height = 0\n buttonParameters.width = 0\n buttonParameters.font_size = 230\n buttonParameters.font_color = Color(0, 0, 0)\n self.createButton(buttonParameters)\n\n -- index 1: option button\n buttonParameters.label = \"Settings\"\n buttonParameters.click_function = \"showOrHideOptions\"\n buttonParameters.color = { 0, 0, 0, 0.96 }\n buttonParameters.position.z = -0.1\n buttonParameters.height = 350\n buttonParameters.width = 1000\n buttonParameters.font_size = 190\n buttonParameters.font_color = \"White\"\n self.createButton(buttonParameters)\n\n -- index 2: start button\n buttonParameters.label = \"Reset play areas\"\n buttonParameters.click_function = \"cleanUp\"\n buttonParameters.position.z = 1.1\n buttonParameters.width = 1550\n self.createButton(buttonParameters)\nend\n\n---------------------------------------------------------\n-- click functions for option buttons\n---------------------------------------------------------\n\n-- changes the UI state and the internal variable for the togglebuttons\nfunction optionButtonClick(_, id)\n local currentState = options[id]\n local newState = (currentState and \"option_off\" or \"option_on\")\n options[id] = not currentState\n self.UI.setAttribute(id, \"image\", newState)\nend\n\n-- shows or hides the option panel\nfunction showOrHideOptions()\n optionsVisible = not optionsVisible\n\n if optionsVisible then\n self.UI.show(\"options\")\n else\n self.UI.hide(\"options\")\n end\nend\n\n---------------------------------------------------------\n-- main function\n---------------------------------------------------------\n\nfunction cleanUp(_, color)\n printToAll(\"------------------------------\", \"White\")\n printToAll(\"Clean up started!\", \"Orange\")\n printToAll(\"Resetting counters...\", \"White\")\n\n getScenarioName()\n soundCubeApi.playSoundByName(\"Vacuum\")\n ignoreCustomDataHelper()\n getTrauma()\n\n -- delay to account for potential state change of campaign log\n Wait.time(updateCounters, 0.2)\n\n resetDoomCounter()\n blessCurseManagerApi.removeAll(color)\n removeLines()\n returnMiniCards()\n discardHands()\n chaosBagApi.returnChaosTokens()\n chaosBagApi.releaseAllSealedTokens(color)\n maybeIgnoreTekeliliCards()\n\n printToAll(\"Tidying main play area...\", \"White\")\n startLuaCoroutine(self, \"tidyPlayareaCoroutine\")\nend\n\n---------------------------------------------------------\n-- modular functions, called by other functions\n---------------------------------------------------------\n\nfunction getScenarioName()\n local tokenData = mythosAreaApi.returnTokenData()\n scenarioName = tokenData.currentScenario\nend\n\nfunction updateCounters()\n if not getOptionValue() then return end\n\n playermatApi.updateCounter(\"All\", \"ResourceCounter\", 5)\n playermatApi.updateCounter(\"All\", \"ClickableClueCounter\", 0)\n playermatApi.resetSkillTracker(\"All\")\n\n for i, color in ipairs(COLORS) do\n playermatApi.updateCounter(color, \"DamageCounter\", RESET_VALUES.Damage[i])\n playermatApi.updateCounter(color, \"HorrorCounter\", RESET_VALUES.Horror[i])\n end\nend\n\n-- reset doom on agenda\nfunction resetDoomCounter()\n local doomCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomCounter\")\n if doomCounter ~= nil then\n doomCounter.call(\"updateVal\")\n else\n printToAll(\"Doom counter could not be found.\", \"Yellow\")\n end\nend\n\n-- adds the ignore tag to the custom data helper\nfunction ignoreCustomDataHelper()\n local customDataHelper = playAreaApi.getCustomDataHelper()\n if customDataHelper then\n customDataHelper.addTag(IGNORE_TAG)\n end\nend\n\n-- read values for trauma from campaign log if enabled\nfunction getTrauma()\n RESET_VALUES = {\n Damage = { 0, 0, 0, 0 },\n Horror = { 0, 0, 0, 0 }\n }\n\n -- stop here if trauma import is disabled\n if not options[\"importTrauma\"] then\n printToAll(\"Default values for health and sanity loaded.\", \"Yellow\")\n return\n end\n\n -- get campaign log\n campaignLog = getObjectsWithTag(\"CampaignLog\")[1]\n if campaignLog == nil then\n printToAll(\"Campaign log not found in standard position!\", \"Yellow\")\n printToAll(\"Default values for health and sanity loaded.\", \"Yellow\")\n return\n end\n loadTrauma()\nend\n\n-- gets data from campaign log if possible\nfunction loadTrauma()\n -- check if \"returnTrauma\" function exists to avoid calling nil\n local trauma = campaignLog.getVar(\"returnTrauma\")\n\n if trauma ~= nil then\n printToAll(\"Trauma values found in campaign log!\", \"Green\")\n trauma = campaignLog.call(\"returnTrauma\")\n for i = 1, 8 do\n if i \u003c 5 then\n RESET_VALUES.Damage[i] = trauma[i]\n else\n RESET_VALUES.Horror[i - 4] = trauma[i]\n end\n end\n loadingFailedBefore = false\n elseif loadingFailedBefore then\n printToAll(\"Trauma values could not be found in campaign log!\", \"Yellow\")\n printToAll(\"Default values for health and sanity loaded.\", \"Yellow\")\n loadingFailedBefore = false\n else\n -- set campaign log to first state\n local stateId = campaignLog.getStateId()\n\n if stateId ~= 1 then\n campaignLog = campaignLog.setState(1)\n end\n loadingFailedBefore = true\n\n -- small delay to account for potential state change\n Wait.time(loadTrauma, 0.1)\n end\nend\n\n-- remove drawn lines\nfunction removeLines()\n if options[\"removeDrawnLines\"] then\n printToAll(\"Removing global vector lines...\", \"White\")\n Global.setVectorLines({})\n end\nend\n\n-- returns the mini cards back to the owning mat\nfunction returnMiniCards()\n -- stop if playermats get tidied anyway\n if getOptionValue() then return end\n\n -- get mini cards in play\n local miniCardIndex = {}\n for _, obj in ipairs(getObjectsWithTag(\"Minicard\")) do\n local notes = JSON.decode(obj.getGMNotes())\n if notes ~= nil and notes.id then\n miniCardIndex[notes.id] = obj\n end\n end\n\n -- move mini cards\n for _, mat in pairs(guidReferenceApi.getObjectsByType(\"Playermat\")) do\n local miniId = mat.getVar(\"activeInvestigatorId\") .. \"-m\"\n if miniCardIndex[miniId] then\n local pos = mat.positionToWorld(Vector(-1.36, 0, -0.625)):setAt(\"y\", 1.67)\n miniCardIndex[miniId].setPosition(pos)\n end\n end\nend\n\n-- discard all hand objects\nfunction discardHands()\n if not getOptionValue() then return end\n for _, color in ipairs(COLORS) do\n local trash = guidReferenceApi.getObjectByOwnerAndType(color, \"Trash\")\n if trash then\n local hand = Player[playermatApi.getPlayerColor(color)].getHandObjects()\n for j = #hand, 1, -1 do\n trash.putObject(hand[j])\n end\n end\n end\nend\n\n-- maybe ignore cards / decks on the tekelili helper\nfunction maybeIgnoreTekeliliCards()\n local tekeliliHelper = getTekeliliHelper()\n if tekeliliHelper then\n removeIgnoreLater = searchLib.onObject(tekeliliHelper, \"isCardOrDeck\")\n for _, obj in ipairs(removeIgnoreLater) do\n obj.addTag(IGNORE_TAG)\n end\n end\nend\n\n-- clean up for play area\nfunction tidyPlayareaCoroutine()\n coWaitFrames(10)\n\n local trash = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n local playAreaZone = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayAreaZone\")\n\n -- reset the playarea image by not providing an image\n playAreaApi.updateSurface()\n\n if playAreaZone == nil then\n printToAll(\"Scripting zone for main play area could not be found!\", \"Red\")\n elseif trash == nil then\n printToAll(\"Trashcan for main play area could not be found!\", \"Red\")\n else\n for _, obj in ipairs(playAreaZone.getObjects()) do\n -- ignore these elements\n if obj.hasTag(IGNORE_TAG) == false\n and obj.locked == false\n and obj.interactable == true then\n coroutine.yield(0)\n trash.putObject(obj)\n end\n end\n end\n\n printToAll(\"Tidying playermats and mythos area...\", \"White\")\n startLuaCoroutine(self, \"tidyPlayerMatCoroutine\")\n return 1\nend\n\n-- clean up for the four playermats and the mythos area\nfunction tidyPlayerMatCoroutine()\n local tekeliliHelper = getTekeliliHelper()\n\n if getOptionValue() then\n for _, color in ipairs(COLORS) do\n local trash = guidReferenceApi.getObjectByOwnerAndType(color, \"Trash\")\n if trash == nil then\n printToAll(\"Trashcan for \" .. color .. \" playermat could not be found! Skipping this playermat.\", \"Yellow\")\n else\n coWaitFrames(20)\n\n -- maybe store tekelili cards\n if tekeliliHelper then\n tekeliliHelper.call(\"storeTekelili\", color)\n end\n\n -- parse playermat objects\n for _, obj in ipairs(playermatApi.searchAroundPlayermat(color)) do\n -- reset action tokens\n if obj.hasTag(\"UniversalToken\") and obj.is_face_down then\n obj.flip()\n end\n\n -- get rid of temporary tokens\n if obj.hasTag(\"Temporary\") then\n trash.putObject(obj)\n end\n\n -- remove objects (with exceptions)\n maybeTrashObject(obj, trash)\n end\n\n -- reset \"activeInvestigatorId\" and \"...class\"\n local mat = guidReferenceApi.getObjectByOwnerAndType(color, \"Playermat\")\n mat.setVar(\"activeInvestigatorId\", \"00000\")\n mat.setVar(\"activeInvestigatorClass\", \"Neutral\")\n mat.call(\"updateTexture\")\n\n coWaitFrames(5)\n\n -- maybe respawn tekelili cards\n if tekeliliHelper then\n tekeliliHelper.call(\"spawnStoredTekelili\", color)\n end\n end\n end\n end\n\n -- mythos area cleanup\n local trash = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"Trash\")\n if trash then\n for _, obj in ipairs(searchLib.inArea({ -2, 2, 10 }, { 0, 270, 0 }, { 55, 1, 13.5 })) do\n maybeTrashObject(obj, trash)\n end\n end\n\n -- maybe remove ignore tag from cards / decks on the tekelili helper\n if removeIgnoreLater then\n for _, obj in ipairs(removeIgnoreLater) do\n if obj ~= nil then\n obj.removeTag(IGNORE_TAG)\n end\n end\n removeIgnoreLater = {}\n end\n\n -- reset spawned data\n tokenSpawnTrackerApi.resetAll()\n local datahelper = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DataHelper\")\n if datahelper then\n datahelper.setTable(\"SPAWNED_PLAYER_CARD_GUIDS\", {})\n end\n\n printToAll(\"Clean up completed!\", \"Green\")\n return 1\nend\n\n-- used to detect the \"Tekeli-li Helper\" for Edge of the Earth\nfunction getTekeliliHelper()\n for _, obj in ipairs(getObjects()) do\n if obj ~= nil and obj.getName() == \"Tekeli-li Helper\" then\n return obj\n end\n end\n return nil\nend\n\nfunction maybeTrashObject(obj, trash)\n if not obj.hasTag(IGNORE_TAG)\n and (not obj.hasTag(\"UniversalToken\") or obj.getScale().x \u003c 0.4)\n and not obj.hasTag(\"chaosBag\")\n and (not obj.locked or obj.hasTag(\"Investigator\"))\n and obj.interactable then\n trash.putObject(obj)\n end\nend\n\n-- get value with respect to override value\nfunction getOptionValue()\n -- don't clean up playermats if playing a prelude from the list\n if preludeList[scenarioName] then\n return false\n else\n return options[\"tidyPlayermats\"]\n end\nend\n\n-- pauses the current coroutine for 'frameCount' frames\nfunction coWaitFrames(frameCount)\n for k = 1, frameCount do\n coroutine.yield(0)\n end\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"core/SoundCubeApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SoundCubeApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n -- this table links the name of a trigger effect to its index\n local soundIndices = {\n [\"Vacuum\"] = 0,\n [\"Deep Bell\"] = 1,\n [\"Dark Souls\"] = 2\n }\n\n ---@param index number Index of the sound effect to play\n local function playTriggerEffect(index)\n local SoundCube = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"SoundCube\")\n SoundCube.AssetBundle.playTriggerEffect(index)\n end\n\n -- plays the by name requested sound\n ---@param soundName string Name of the sound to play\n SoundCubeApi.playSoundByName = function(soundName)\n playTriggerEffect(soundIndices[soundName])\n end\n\n return SoundCubeApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"options\":{\"importTrauma\":true,\"removeDrawnLines\":false,\"tidyPlayermats\":true}}", "MeasureMovement": false, "Name": "Custom_Token", @@ -208465,7 +145960,7 @@ "scaleZ": 1.5 }, "Value": 0, - "XmlUI": "\u003c!-- include accessories/CleanUpHelper.xml --\u003e\n\u003c!-- Default formatting --\u003e\n\u003cDefaults\u003e\n \u003cText color=\"black\" alignment=\"MiddleLeft\"/\u003e\n \u003cText class=\"h1\" fontSize=\"160\" font=\"font_teutonic-arkham\"/\u003e\n \u003cText class=\"h2\" fontSize=\"120\" font=\"font_teutonic-arkham\"/\u003e\n \u003cText class=\"p\" fontSize=\"60\" alignment=\"UpperLeft\"/\u003e\n\n \u003cPanel rotation=\"0 0 180\"/\u003e\n \u003cPanel class=\"window\" width=\"1500\" height=\"1500\" color=\"white\" outline=\"white\" outlineSize=\"10 10\"/\u003e\n\n \u003cRow dontUseTableRowBackground=\"true\"/\u003e\n \u003cRow class=\"header\" color=\"#707070\"/\u003e\n \u003cRow class=\"option\" preferredHeight=\"200\" color=\"#9e9e9e\"/\u003e\n\n \u003c!-- row heights: 70 x lines + 50 --\u003e\n \u003cRow class=\"description\" color=\"#cfcfcf\"/\u003e\n\n \u003cButton class=\"optionToggle\" rectAlignment=\"MiddleRight\" offsetXY=\"-30 0\" colors=\"#FFFFFF|#dfdfdf\" height=\"160\" width=\"288\" ignoreLayout=\"True\" fontSize=\"60\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- Option window --\u003e\n\u003cPanel id=\"options\" class=\"window\" offsetXY=\"-580 200\" scale=\"0.5 0.5\" active=\"false\" showAnimation=\"FadeIn\" hideAnimation=\"FadeOut\"\u003e\n \u003cTableLayout cellPadding=\"25 25 15 15\"\u003e\n \u003c!-- Header --\u003e\n \u003cRow class=\"header\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h1\"\u003eClean up Helper - Options\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option --\u003e\n \u003cRow class=\"option\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h2\"\u003eImport trauma\u003c/Text\u003e\n \u003cButton class=\"optionToggle\" id=\"importTrauma\" onClick=\"optionButtonClick(importTrauma)\" image=\"option_on\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow class=\"description\" preferredHeight=\"330\"\u003e\n \u003cCell\u003e\n \u003cText class=\"p\"\u003eEnables importing trauma values from the campaign log (custom content might give wrong values!).\u0026#xA;Enter players in the campaign log in this order:\u0026#xA;White, Orange, Green, Red.\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option --\u003e\n \u003cRow class=\"option\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h2\"\u003eTidy playermats\u003c/Text\u003e\n \u003cButton class=\"optionToggle\" id=\"tidyPlayermats\" onClick=\"optionButtonClick(tidyPlayermats)\" image=\"option_on\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow class=\"description\" preferredHeight=\"190\"\u003e\n \u003cCell\u003e\n \u003cText class=\"p\"\u003eControls whether the playermats should get tidied (removal of all cards and tokens).\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option --\u003e\n \u003cRow class=\"option\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h2\"\u003eRemove drawn lines\u003c/Text\u003e\n \u003cButton class=\"optionToggle\" id=\"removeDrawnLines\" onClick=\"optionButtonClick(removeDrawnLines)\" image=\"option_off\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow class=\"description\" preferredHeight=\"120\"\u003e\n \u003cCell\u003e\n \u003cText class=\"p\"\u003eControls whether all drawn lines should be removed.\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n\u003c/Panel\u003e\n\u003c!-- include accessories/CleanUpHelper.xml --\u003e" + "XmlUI": "\u003c!-- include accessories/CleanUpHelper.xml --\u003e\n\u003c!-- Default formatting --\u003e\n\u003cDefaults\u003e\n \u003cText color=\"black\"\n alignment=\"MiddleLeft\"/\u003e\n \u003cText class=\"h1\"\n fontSize=\"160\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cText class=\"h2\"\n fontSize=\"120\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cText class=\"p\"\n fontSize=\"60\"\n alignment=\"UpperLeft\"/\u003e\n\n \u003cPanel rotation=\"0 0 180\"/\u003e\n \u003cPanel class=\"window\"\n width=\"1500\"\n height=\"1500\"\n color=\"white\"\n outline=\"white\"\n outlineSize=\"10 10\"/\u003e\n\n \u003cRow dontUseTableRowBackground=\"true\"/\u003e\n \u003cRow class=\"header\"\n color=\"#707070\"/\u003e\n \u003cRow class=\"option\"\n preferredHeight=\"200\"\n color=\"#9e9e9e\"/\u003e\n\n \u003c!-- row heights: 70 x lines + 50 --\u003e\n \u003cRow class=\"description\"\n color=\"#cfcfcf\"/\u003e\n\n \u003cButton class=\"optionToggle\"\n rectAlignment=\"MiddleRight\"\n offsetXY=\"-30 0\"\n colors=\"#FFFFFF|#dfdfdf\"\n height=\"160\"\n width=\"288\"\n ignoreLayout=\"True\"\n fontSize=\"60\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- Option window --\u003e\n\u003cPanel id=\"options\"\n class=\"window\"\n offsetXY=\"-580 200\"\n scale=\"0.5 0.5\"\n active=\"false\"\n showAnimation=\"FadeIn\"\n hideAnimation=\"FadeOut\"\u003e\n \u003cTableLayout cellPadding=\"25 25 15 15\"\u003e\n \u003c!-- Header --\u003e\n \u003cRow class=\"header\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h1\"\u003eClean up Helper - Options\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option --\u003e\n \u003cRow class=\"option\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h2\"\u003eImport trauma\u003c/Text\u003e\n \u003cButton class=\"optionToggle\"\n id=\"importTrauma\"\n onClick=\"optionButtonClick(importTrauma)\"\n image=\"option_on\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow class=\"description\"\n preferredHeight=\"330\"\u003e\n \u003cCell\u003e\n \u003cText class=\"p\"\u003eEnables importing trauma values from the campaign log (custom content might give wrong values!).\u0026#xA;Enter players in the campaign log in this order:\u0026#xA;White, Orange, Green, Red.\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option --\u003e\n \u003cRow class=\"option\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h2\"\u003eTidy playermats\u003c/Text\u003e\n \u003cButton class=\"optionToggle\"\n id=\"tidyPlayermats\"\n onClick=\"optionButtonClick(tidyPlayermats)\"\n image=\"option_on\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow class=\"description\"\n preferredHeight=\"190\"\u003e\n \u003cCell\u003e\n \u003cText class=\"p\"\u003eControls whether the playermats should get tidied (removal of all cards and tokens).\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option --\u003e\n \u003cRow class=\"option\"\u003e\n \u003cCell\u003e\n \u003cText class=\"h2\"\u003eRemove drawn lines\u003c/Text\u003e\n \u003cButton class=\"optionToggle\"\n id=\"removeDrawnLines\"\n onClick=\"optionButtonClick(removeDrawnLines)\"\n image=\"option_off\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003cRow class=\"description\"\n preferredHeight=\"120\"\u003e\n \u003cCell\u003e\n \u003cText class=\"p\"\u003eControls whether all drawn lines should be removed.\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n\u003c/Panel\u003e\n\u003c!-- include accessories/CleanUpHelper.xml --\u003e" } ], "Description": "Contains the objects that are spawnable via option panel", @@ -208555,51 +146050,6 @@ "Value": 0, "XmlUI": "" }, - { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 0.66176, - "g": 0.66176, - "r": 0.66176 - }, - "Description": "This object contains \"Game Keys\" that can be assigned via Options --\u003e Game Keys.", - "DragSelectable": true, - "GMNotes": "", - "GUID": "fce69c", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GameKeyHandler\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal blessCurseManagerApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal navigationOverlayApi = require(\"core/NavigationOverlayApi\")\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\nlocal searchLib = require(\"util/SearchLib\")\nlocal victoryDisplayApi = require(\"core/VictoryDisplayApi\")\n\nfunction onLoad()\n addHotkey(\"Add doom to agenda\", addDoomToAgenda)\n addHotkey(\"Add Bless/Curse context menu\", addBlurseSealingMenu)\n addHotkey(\"Discard object\", discardObject)\n addHotkey(\"Discard top card\", discardTopDeck)\n addHotkey(\"Display Bless/Curse status\", showBlessCurseStatus)\n addHotkey(\"Move card to Victory Display\", moveCardToVictoryDisplay)\n addHotkey(\"Place card into threat area\", takeCardIntoThreatArea)\n addHotkey(\"Remove a use\", removeOneUse)\n addHotkey(\"Switch seat clockwise\", switchSeatClockwise)\n addHotkey(\"Switch seat counter-clockwise\", switchSeatCounterClockwise)\n addHotkey(\"Take clue from location\", takeClueFromLocation)\n addHotkey(\"Take clue from location (White)\", takeClueFromLocationWhite)\n addHotkey(\"Take clue from location (Orange)\", takeClueFromLocationOrange)\n addHotkey(\"Take clue from location (Green)\", takeClueFromLocationGreen)\n addHotkey(\"Take clue from location (Red)\", takeClueFromLocationRed)\n addHotkey(\"Upkeep\", triggerUpkeep)\n addHotkey(\"Upkeep (Multi-handed)\", triggerUpkeepMultihanded)\nend\n\n-- triggers the \"Upkeep\" function of the calling player's playmat\nfunction triggerUpkeep(playerColor)\n if playerColor == \"Black\" then\n broadcastToColor(\"Triggering 'Upkeep (Multihanded)' instead\", playerColor, \"Yellow\")\n triggerUpkeepMultihanded(playerColor)\n return\n end\n local matColor = playmatApi.getMatColor(playerColor)\n playmatApi.doUpkeepFromHotkey(matColor, playerColor)\nend\n\n-- triggers the \"Upkeep\" function of the calling player's playmat AND\n-- for all playmats that don't have a seated player, but an investigator card\nfunction triggerUpkeepMultihanded(playerColor)\n if playerColor ~= \"Black\" then\n triggerUpkeep(playerColor)\n end\n local colors = Player.getAvailableColors()\n for _, handColor in ipairs(colors) do\n local matColor = playmatApi.getMatColor(handColor)\n if playmatApi.returnInvestigatorId(matColor) ~= \"00000\" and Player[handColor].seated == false then\n playmatApi.doUpkeepFromHotkey(matColor, playerColor)\n end\n end\nend\n\n-- adds 1 doom to the agenda\nfunction addDoomToAgenda()\n local doomCounter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DoomCounter\")\n doomCounter.call(\"addVal\", 1)\nend\n\n-- move the hovered object to the nearest empty slot on the playermat\nfunction takeCardIntoThreatArea(playerColor, hoveredObject)\n -- only continue if an unlocked card\n if hoveredObject == nil\n or hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Deck\"\n or hoveredObject.hasTag(\"Location\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a non-location card and try again.\", playerColor, \"Yellow\")\n return\n end\n\n local matColor = playmatApi.getMatColor(playerColor)\n local mat = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\")\n -- do not continue if the threat area is already full\n if playmatApi.getEncounterCardDrawPosition(matColor, false) == playmatApi.getEncounterCardDrawPosition(matColor, true) then \n broadcastToColor(\"Threat area is full.\", playerColor,\"Yellow\")\n return \n end\n \n \n -- initialize list of objects to move\n local moveTheseObjects = {}\n \n for _, obj in ipairs(searchLib.onObject(hoveredObject, \"isTileOrToken\")) do\n table.insert(moveTheseObjects, obj)\n end\n \n -- find out if the original card is on the green or red playmats\n local originalMatColor = guidReferenceApi.getOwnerOfObject(hoveredObject)\n \n -- determine modifiers for the playmats\n local modifierY\n if originalMatColor == \"Red\" then\n modifierY = 90\n elseif originalMatColor == \"Green\" then\n modifierY = -90\n else\n modifierY = 0\n end\n\n local localPositions = {}\n\n for i, obj in ipairs(moveTheseObjects) do\n local localPos = hoveredObject.positionToLocal(obj.getPosition())\n localPositions[i] = localPos\n end\n \n -- move the main card\n local pos = playmatApi.getEncounterCardDrawPosition(matColor, false)\n hoveredObject.setPosition(pos)\n hoveredObject.setRotation(hoveredObject.getRotation() - Vector(0, 270-mat.getRotation().y-modifierY, 0))\n \n local cardName = hoveredObject.getName()\n if cardName == nil or cardName == \"\" then\n cardName = \"card(s)\"\n end\n broadcastToAll(\"Placed \" .. cardName .. \" into threat area.\", \"White\")\n\n for i, obj in ipairs(moveTheseObjects) do\n if not obj.locked then\n local globalPos = hoveredObject.positionToWorld(localPositions[i])\n obj.setPosition(globalPos)\n obj.setRotation(obj.getRotation() - Vector(0, 270-mat.getRotation().y-modifierY, 0))\n end\n end\nend\n\n-- discard the hovered object to the respective trashcan and discard tokens on it if it was a card\nfunction discardObject(playerColor, hoveredObject)\n -- only continue if an unlocked card, deck or tile was hovered\n if hoveredObject == nil\n or (hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Deck\" and hoveredObject.type ~= \"Tile\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a token/tile or a card/deck and try again.\", playerColor, \"Yellow\")\n return\n end\n\n -- warning for locations since these are usually not meant to be discarded\n if hoveredObject.hasTag(\"Location\") then\n broadcastToAll(\"Watch out: A location was discarded.\", \"Yellow\")\n end\n\n -- initialize list of objects to discard\n local discardTheseObjects = { hoveredObject }\n\n -- discard tokens / tiles on cards / decks\n if hoveredObject.type ~= \"Tile\" then\n for _, obj in ipairs(searchLib.onObject(hoveredObject, \"isTileOrToken\")) do\n table.insert(discardTheseObjects, obj)\n end\n end\n\n local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor)\n playmatApi.discardListOfObjects(discardForMatColor, discardTheseObjects)\nend\n\n-- discard the top card of hovered deck, calling discardObject function\nfunction discardTopDeck(playerColor, hoveredObject)\n -- only continue if an unlocked card or deck was hovered\n if hoveredObject == nil\n or (hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Deck\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a deck/card and try again.\", playerColor, \"Yellow\")\n return\n end\n if hoveredObject.type == \"Deck\" then\n takenCard = hoveredObject.takeObject({index = 0})\n else\n takenCard = hoveredObject\n end\n Wait.frames(function() discardObject(playerColor, takenCard) end, 1)\nend \n\n-- helper function to get the player to trigger the discard function for\nfunction getColorToDiscardFor(hoveredObject, playerColor)\n local pos = hoveredObject.getPosition()\n local closestMatColor = playmatApi.getMatColorByPosition(pos)\n\n -- check if actually on the closest playmat\n local closestMat = guidReferenceApi.getObjectByOwnerAndType(closestMatColor, \"Playermat\")\n local bounds = closestMat.getBounds()\n\n -- define the area \"near\" the playmat\n local bufferAroundPlaymat = 2\n local areaNearPlaymat = {}\n areaNearPlaymat.minX = bounds.center.x - bounds.size.x / 2 - bufferAroundPlaymat\n areaNearPlaymat.maxX = bounds.center.x + bounds.size.x / 2 + bufferAroundPlaymat\n areaNearPlaymat.minZ = bounds.center.z - bounds.size.z / 2 - bufferAroundPlaymat\n areaNearPlaymat.maxZ = bounds.center.z + bounds.size.z / 2 + bufferAroundPlaymat\n\n -- discard to closest mat if near it, use triggering playmat if not\n local discardForMatColor\n if inArea(pos, areaNearPlaymat) then\n return closestMatColor\n elseif pos.y \u003e (Player[playerColor].getHandTransform().position.y - (Player[playerColor].getHandTransform().scale.y / 2)) then -- discard to closest mat if card is in a hand\n return closestMatColor\n else\n return playmatApi.getMatColor(playerColor)\n end\nend\n\n-- moves the hovered card to the victory display\nfunction moveCardToVictoryDisplay(_, hoveredObject)\n victoryDisplayApi.placeCard(hoveredObject)\nend\n\n-- removes a use from a card (or a token if hovered)\nfunction removeOneUse(playerColor, hoveredObject)\n -- only continue if an unlocked card or tile was hovered\n if hoveredObject == nil\n or (hoveredObject.type ~= \"Card\" and hoveredObject.type ~= \"Tile\")\n or hoveredObject.locked then\n broadcastToColor(\"Hover a token/tile or a card and try again.\", playerColor, \"Yellow\")\n return\n end\n\n local targetObject = nil\n\n -- discard hovered token / tile\n if hoveredObject.type == \"Tile\" then\n targetObject = hoveredObject\n elseif hoveredObject.type == \"Card\" then\n -- grab the first use type from the metadata (or nil)\n local notes = JSON.decode(hoveredObject.getGMNotes()) or {}\n local usesData = notes.uses or {}\n local useInfo = usesData[1] or {}\n local searchForType = useInfo.type\n if searchForType then searchForType = searchForType:lower() end\n\n for _, obj in ipairs(searchLib.onObject(hoveredObject, \"isTileOrToken\")) do\n if not obj.locked and obj.memo ~= \"resourceCounter\" then\n -- check for matching object, otherwise use the first hit\n if obj.memo == searchForType then\n targetObject = obj\n break\n elseif not targetObject then\n targetObject = obj\n end\n end\n end\n end\n\n -- error handling\n if not targetObject then\n broadcastToColor(\"No tokens found!\", playerColor, \"Yellow\")\n return\n end\n\n -- handling for stacked tokens\n if targetObject.getQuantity() \u003e 1 then\n targetObject = targetObject.takeObject()\n end\n\n -- feedback message\n local tokenName = targetObject.getName()\n if tokenName == \"\" then\n if targetObject.memo ~= \"\" then\n -- name handling for clue / doom\n if targetObject.memo == \"clueDoom\" then\n if targetObject.is_face_down then\n tokenName = \"Doom\"\n else\n tokenName = \"Clue\"\n end\n else\n tokenName = titleCase(targetObject.memo)\n end\n else\n tokenName = \"Unknown\"\n end\n end\n\n local playerName = Player[playerColor].steam_name\n broadcastToAll(playerName .. \" removed a token: \" .. tokenName, playerColor)\n\n local discardForMatColor = getColorToDiscardFor(hoveredObject, playerColor)\n playmatApi.discardListOfObjects(discardForMatColor, { targetObject })\nend\n\n-- switches the triggering player to the next seat (clockwise)\nfunction switchSeatClockwise(playerColor)\n switchSeat(playerColor, \"clockwise\")\nend\n\n-- switches the triggering player to the next seat (counter-clockwise)\nfunction switchSeatCounterClockwise(playerColor)\n switchSeat(playerColor, \"counter-clockwise\")\nend\n\n-- handles seat switching in the given direction\nfunction switchSeat(playerColor, direction)\n if playerColor == \"Black\" or playerColor == \"Grey\" then\n broadcastToColor(\"This hotkey is only available to seated players.\", playerColor, \"Orange\")\n return\n end\n\n -- sort function for matcolors based on hand position (Green, White, Orange, Red)\n local function sortByHandPosition(color1, color2)\n local pos1 = Player[color1].getHandTransform().position\n local pos2 = Player[color2].getHandTransform().position\n return pos1.z \u003e pos2.z\n end\n\n -- get used playermats\n local usedColors = playmatApi.getUsedMatColors()\n table.sort(usedColors, sortByHandPosition)\n\n -- get current seat index\n local index\n for i, color in ipairs(usedColors) do\n if color == playerColor then\n index = i\n break\n end\n end\n if not index then\n broadcastToColor(\"Couldn't detect investigator.\", playerColor, \"Orange\")\n return\n end\n\n -- get next color\n index = index + ((direction == \"clockwise\") and -1 or 1)\n if index == 0 then\n index = #usedColors\n elseif index \u003e #usedColors then\n index = 1\n end\n\n -- swap color\n navigationOverlayApi.loadCamera(Player[playerColor], usedColors[index])\nend\n\nfunction takeClueFromLocationWhite(_, hoveredObject)\n takeClueFromLocation(\"White\", hoveredObject)\nend\n\nfunction takeClueFromLocationOrange(_, hoveredObject)\n takeClueFromLocation(\"Orange\", hoveredObject)\nend\n\nfunction takeClueFromLocationGreen(_, hoveredObject)\n takeClueFromLocation(\"Green\", hoveredObject)\nend\n\nfunction takeClueFromLocationRed(_, hoveredObject)\n takeClueFromLocation(\"Red\", hoveredObject)\nend\n\n-- takes a clue from a location, player needs to hover the clue directly or the location\nfunction takeClueFromLocation(playerColor, hoveredObject)\n -- use different color for messages if player is not seated (because this hotkey is called for a different mat)\n local messageColor = playerColor\n if not Player[playerColor] or not Player[playerColor].seated then\n messageColor = getFirstSeatedPlayer()\n end\n\n local cardName, clue\n\n if hoveredObject == nil then\n broadcastToColor(\"Hover a clue or card with clues and try again.\", messageColor, \"Yellow\")\n return\n elseif hoveredObject.type == \"Card\" then\n cardName = hoveredObject.getName()\n local searchResult = searchLib.onObject(hoveredObject, \"isClue\")\n\n if #searchResult == 0 then\n broadcastToColor(\"This card does not have any clues on it.\", messageColor, \"Yellow\")\n return\n else\n clue = searchResult[1]\n end\n elseif hoveredObject.memo == \"clueDoom\" then\n if hoveredObject.is_face_down then\n broadcastToColor(\"This is a doom token and not a clue.\", messageColor, \"Yellow\")\n return\n end\n\n clue = hoveredObject\n local searchResult = searchLib.belowPosition(clue.getPosition(), \"isCard\")\n\n if #searchResult ~= 0 then\n cardName = searchResult[1].getName()\n end\n elseif hoveredObject.type == \"Infinite\" and hoveredObject.getName() == \"Clue tokens\" then\n clue = hoveredObject.takeObject()\n cardName = \"token pool\"\n else\n broadcastToColor(\"Hover a clue or card with clues and try again.\", messageColor, \"Yellow\")\n return\n end\n\n local clickableClues = optionPanelApi.getOptions()[\"useClueClickers\"]\n\n -- handling for calling this for a specific mat via hotkey\n local playerName, matColor, pos\n if Player[playerColor] and Player[playerColor].seated then\n playerName = Player[playerColor].steam_name\n matColor = playmatApi.getMatColor(playerColor)\n else\n playerName = playerColor\n matColor = playerColor\n end\n\n if clickableClues then\n pos = {x = 0.49, y = 2.66, z = 0.00}\n playmatApi.updateCounter(matColor, \"ClickableClueCounter\", _, 1)\n else\n pos = playmatApi.transformLocalPosition({x = -1.12, y = 0.05, z = 0.7}, matColor)\n end\n \n local rot = playmatApi.returnRotation(matColor)\n\n -- check if found clue is a stack or single token\n if clue.getQuantity() \u003e 1 then\n clue.takeObject({position = pos, rotation = rot})\n else\n clue.setPositionSmooth(pos)\n clue.setRotation(rot)\n end\n\n if cardName then\n broadcastToAll(playerName .. \" took one clue from \" .. cardName .. \".\", \"White\")\n else\n broadcastToAll(playerName .. \" took one clue.\", \"White\")\n end\n\n victoryDisplayApi.update()\nend\n\n-- broadcasts the bless/curse status to the calling player\nfunction showBlessCurseStatus(playerColor)\n blessCurseManagerApi.broadcastStatus(playerColor)\nend\n\n-- adds Wendy's menu to the hovered card\nfunction addBlurseSealingMenu(playerColor, hoveredObject)\n blessCurseManagerApi.addBlurseSealingMenu(playerColor, hoveredObject)\nend\n\n-- Simple method to check if the given point is in a specified area\n---@param point tts__Vector Point to check, only x and z values are relevant\n---@param bounds table Defined area to see if the point is within\nfunction inArea(point, bounds)\n return (point.x \u003e bounds.minX\n and point.x \u003c bounds.maxX\n and point.z \u003e bounds.minZ\n and point.z \u003c bounds.maxZ)\nend\n\n-- capitalizes the first letter\nfunction titleCase(str)\n local first = str:sub(1, 1)\n local rest = str:sub(2)\n return first:upper() .. rest:lower()\nend\n\n-- returns the color of the first seated player\nfunction getFirstSeatedPlayer()\n for _, color in ipairs(getSeatedPlayers()) do\n return color\n end\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/VictoryDisplayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local VictoryDisplayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getVictoryDisplay()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"VictoryDisplay\")\n end\n\n -- triggers an update of the victory count\n ---@param delay? number Delay in seconds after which the update call is executed\n VictoryDisplayApi.update = function(delay)\n getVictoryDisplay().call(\"startUpdate\", delay)\n end\n\n -- moves a card to the victory display (in the first empty spot)\n ---@param object tts__Object Object that should be checked and potentially moved\n VictoryDisplayApi.placeCard = function(object)\n if object ~= nil and object.tag == \"Card\" then\n getVictoryDisplay().call(\"placeCard\", object)\n end\n end\n\n return VictoryDisplayApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/GameKeyHandler\")\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/NavigationOverlayApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local NavigationOverlayApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getNOHandler()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"NavigationOverlayHandler\")\n end\n\n -- copies the visibility for the Navigation overlay\n ---@param startColor string Color of the player to copy from\n ---@param targetColor string Color of the targeted player\n NavigationOverlayApi.copyVisibility = function(startColor, targetColor)\n getNOHandler().call(\"copyVisibility\", {\n startColor = startColor,\n targetColor = targetColor\n })\n end\n\n -- changes the Navigation Overlay view (\"Full View\" --\u003e \"Play Areas\" --\u003e \"Closed\" etc.)\n ---@param playerColor string Color of the player to update the visibility for\n NavigationOverlayApi.cycleVisibility = function(playerColor)\n getNOHandler().call(\"cycleVisibility\", playerColor)\n end\n\n -- loads the specified camera for a player\n ---@param player tts__Player Player whose camera should be moved\n ---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\n NavigationOverlayApi.loadCamera = function(player, camera)\n getNOHandler().call(\"loadCameraFromApi\", {\n player = player,\n camera = camera\n })\n end\n\n return NavigationOverlayApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "go_game_piece_white", - "Nickname": "Game Key Handler", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": 78, - "posY": 1.328, - "posZ": -10, - "rotX": 0, - "rotY": 0, - "rotZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - }, - "Value": 0, - "XmlUI": "" - }, { "AltLookAngle": { "x": 0, @@ -208649,7 +146099,7 @@ ], "Tooltip": true, "Transform": { - "posX": -48, + "posX": -66, "posY": 1.48, "posZ": 55, "rotX": 0, @@ -208739,7 +146189,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/NavigationOverlayHandler\")\nend)\n__bundle_register(\"core/NavigationOverlayHandler\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\nfullButtonData = {\n { id = \"1\", width = \"84\", height = \"33\", offset = \"1 2\" }, -- 1. Act/Agenda\n { id = \"2\", width = \"78\", height = \"69\", offset = \"1 -62\" }, -- 2. Map\n { id = \"3\", width = \"70\", height = \"36\", offset = \"-38 -126\" }, -- 3. White\n { id = \"4\", width = \"70\", height = \"36\", offset = \"38 -126\" }, -- 4. Orange\n { id = \"5\", width = \"36\", height = \"70\", offset = \"-63 -66\" }, -- 5. Green\n { id = \"6\", width = \"36\", height = \"70\", offset = \"63 -66\" }, -- 6. Red\n { id = \"7\", width = \"38\", height = \"38\", offset = \"-65 -3\" }, -- 7. Victory\n { id = \"8\", width = \"40\", height = \"40\", offset = \"65 -3\" }, -- 8. Guide\n { id = \"9\", width = \"56\", height = \"16\", offset = \"1 -20\" }, -- 9. Player count\n { id = \"10\", width = \"36\", height = \"16\", offset = \"1 -102\" }, -- 10. Bless/Curse\n { id = \"11\", width = \"168\", height = \"56\", offset = \"1 47\" }, -- 11. Scenarios\n { id = \"12\", width = \"52\", height = \"53\", offset = \"-154 134\" }, -- 12. Player card panel\n { id = \"13\", width = \"22\", height = \"22\", offset = \"-116 132\" }, -- 13. Search card panel\n { id = \"14\", width = \"120\", height = \"75\", offset = \"-152 70\" }, -- 14. Player card display\n { id = \"15\", width = \"40\", height = \"54\", offset = \"-150 -38\" }, -- 15. Deck builder\n { id = \"16\", width = \"104\", height = \"84\", offset = \"-154 -114\" }, -- 16. Rules area\n { id = \"17\", width = \"100\", height = \"170\", offset = \"152 72\" }, -- 17. Cycle area\n { id = \"18\", width = \"56\", height = \"60\", offset = \"182 -124\" }, -- 18. Additions\n { id = \"19\", width = \"20\", height = \"20\", offset = \"0 150\" }, -- 19. Shrink\n { id = \"20\", width = \"20\", height = \"20\", offset = \"20 150\" }, -- 20. Close\n { id = \"21\", width = \"20\", height = \"20\", offset = \"-20 150\" } -- 21. Settings\n}\n\nplayButtonData = {\n { id = \"1\", width = \"80\", height = \"33\", offset = \"0 55\" },\n { id = \"2\", width = \"78\", height = \"70\", offset = \"0 -8\" },\n { id = \"3\", width = \"68\", height = \"32\", offset = \"-36 -71\" },\n { id = \"4\", width = \"68\", height = \"32\", offset = \"36 -71\" },\n { id = \"5\", width = \"35\", height = \"66\", offset = \"-65 -10\" },\n { id = \"6\", width = \"35\", height = \"66\", offset = \"65 -10\" },\n { id = \"7\", width = \"38\", height = \"38\", offset = \"-66 52\" },\n { id = \"8\", width = \"38\", height = \"38\", offset = \"66 52\" },\n { id = \"9\", width = \"50\", height = \"12\", offset = \"0 33\" },\n { id = \"10\", width = \"32\", height = \"12\", offset = \"0 -48\" },\n { id = \"19\", width = \"20\", height = \"20\", offset = \"0 80\" },\n { id = \"20\", width = \"20\", height = \"20\", offset = \"20 80\" },\n { id = \"21\", width = \"20\", height = \"20\", offset = \"-20 80\" }\n}\n\n-- To-Do: dynamically get positions by linking to objects\ncameraData = {\n { position = { -1.6, 1.55, 0 }, distance = 18 }, -- 1. Act/Agenda\n { position = { -28, 1.55, 0 }, distance = -1 }, -- 2. Map\n { position = { -31.6, 1.55, 26.4 }, distance = -1 }, -- 3. Green playmat\n { position = { -55, 1.55, 12.05 }, distance = -1 }, -- 4. White playmat\n { position = { -55, 1.55, -11.48 }, distance = -1 }, -- 5. Orange playmat\n { position = { -31.6, 1.55, -26.4 }, distance = -1 }, -- 6. Red playmat\n { position = { -3, 1.55, 30 }, distance = 16 }, -- 7. Victory / SetAside\n { position = { -3, 1.55, -26.76 }, distance = 16 }, -- 8. Guide\n { position = { -11.83, 1.55, 0 }, distance = 10 }, -- 9. Player count\n { position = { -48.35, 1.55, 0 }, distance = 10 }, -- 10. Bless/Curse\n { position = { 12.56, 1.55, 0 }, distance = 45 }, -- 11. Scenarios\n { position = { 57.8, 1.55, 71 }, distance = 22 }, -- 12. Player card panel\n { position = { 60.38, 1.55, 56 }, distance = 10 }, -- 13. Card search panel\n { position = { 27.48, 1.55, 71 }, distance = 35 }, -- 14. Player card area\n { position = { -19.48, 1.55, 71 }, distance = 22 }, -- 15. Deck builder\n { position = { -52.92, 1.55, 71 }, distance = 42 }, -- 16. Rules area\n { position = { 26, 1.55, -71 }, distance = 65 }, -- 17. Cycle area\n { position = { -59.08, 1.55, -83 }, distance = 27 } -- 18. Additions\n}\n\nlocal settingsOpenForColor\nlocal visibility = {}\nlocal claims = {}\nlocal pitch = {}\nlocal distance = {}\n\n---------------------------------------------------------\n-- save/load functionality\n---------------------------------------------------------\n\nfunction onSave()\n return JSON.encode({\n visibility = visibility,\n claims = claims,\n pitch = pitch,\n distance = distance\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n visibility = loadedData.visibility\n claims = loadedData.claims\n pitch = loadedData.pitch\n distance = loadedData.distance\n else\n local allColors = Player.getColors()\n\n for _, color in ipairs(allColors) do\n -- default state for claims\n claims[color] = {}\n\n -- default state for visibility\n visibility[color] = { full = false, play = false }\n end\n end\n\n createXmlButtons()\n updateVisibility()\nend\n\n---------------------------------------------------------\n-- visibility related functions\n---------------------------------------------------------\n\nfunction cycleVisibility(color)\n setVisibility(\"next\", color)\nend\n\nfunction copyVisibility(params)\n visibility[params.targetColor] = {\n full = visibility[params.startColor].full,\n play = visibility[params.startColor].play\n }\n updateVisibility()\nend\n\nfunction setVisibility(type, color)\n if type == \"next\" then\n if visibility[color].full then\n visibility[color] = { full = false, play = true }\n elseif visibility[color].play then\n visibility[color] = { full = false, play = false }\n else\n visibility[color] = { full = true, play = false }\n end\n elseif type == \"toggle\" then\n visibility[color] = {\n full = not visibility[color].full,\n play = not visibility[color].play\n }\n else\n visibility[color] = { full = false, play = false }\n end\n\n updateVisibility()\nend\n\n-- update XML visibility\nfunction updateVisibility()\n local colorString = { full = \"\", play = \"\" }\n\n for color, v in pairs(visibility) do\n if v.full then\n if colorString.full == \"\" then\n colorString.full = color\n else\n colorString.full = colorString.full .. '|' .. color\n end\n elseif v.play then\n if colorString.play == \"\" then\n colorString.play = color\n else\n colorString.play = colorString.play .. '|' .. color\n end\n end\n end\n\n -- update the visibility on the XML\n UI.setAttribute(\"navPanelFull\", \"visibility\", colorString.full)\n UI.setAttribute(\"navPanelPlay\", \"visibility\", colorString.play)\n UI.setAttribute(\"navPanelFull\", \"active\", colorString.full ~= \"\")\n UI.setAttribute(\"navPanelPlay\", \"active\", colorString.play ~= \"\")\nend\n\n---------------------------------------------------------\n-- XML button creation\n---------------------------------------------------------\n\nfunction createXmlButtons()\n local ui = UI.getXmlTable()\n ui = createXmlButtonHelper(ui, {\n data = fullButtonData,\n id = \"navPanelFull\",\n overlay = \"OverlayLarge\"\n })\n ui = createXmlButtonHelper(ui, {\n data = playButtonData,\n id = \"navPanelPlay\",\n overlay = \"OverlaySmall\"\n })\n UI.setXmlTable(ui)\nend\n\n-- XML button creation\nfunction createXmlButtonHelper(ui, params)\n local color\n local guid = self.getGUID()\n local xml = findTagWithId(ui, params.id)\n\n -- add basic image\n xml.children = { {\n tag = \"image\",\n attributes = {\n id = \"backgroundImage\",\n image = params.overlay\n }\n } }\n\n -- add all buttons\n for _, d in ipairs(params.data) do\n table.insert(xml.children, {\n tag = \"button\",\n attributes = {\n onClick = guid .. \"/buttonClicked\",\n id = d.id,\n height = d.height,\n width = d.width,\n offsetXY = d.offset,\n color = \"rgba(0,1,0,0)\"\n }\n })\n end\n return ui\nend\n\nfunction findTagWithId(ui, id)\n for _, obj in ipairs(ui) do\n if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end\n if obj.children then\n local result = findTagWithId(obj.children, id)\n if result then return result end\n end\n end\n return nil\nend\n\n---------------------------------------------------------\n-- core functionality\n---------------------------------------------------------\n\n-- handles all button clicks\nfunction buttonClicked(player, _, id)\n local index = tonumber(id) or \"\"\n\n if index == 19 then\n setVisibility(\"toggle\", player.color)\n elseif index == 20 then\n setVisibility(\"close\", player.color)\n elseif index == 21 then\n toggleSettings(player)\n else\n loadCamera(player, index)\n end\nend\n\n-- generates a table with rectangular bounds for provided objects\nfunction getDynamicViewBounds(objList)\n local count = 0\n local totalBounds = {\n minX = 0,\n maxX = -70,\n minZ = 60,\n maxZ = -60\n }\n\n for _, obj in pairs(objList) do\n if not obj.hasTag(\"CameraZoom_ignore\") and not obj.hasTag(\"CampaignLog\") then\n count = count + 1\n local bounds = obj.getBounds()\n local x1 = bounds['center'][1] - bounds['size'][1] / 2\n local x2 = bounds['center'][1] + bounds['size'][1] / 2\n local z1 = bounds['center'][3] - bounds['size'][3] / 2\n local z2 = bounds['center'][3] + bounds['size'][3] / 2\n\n totalBounds.minX = math.min(x1, totalBounds.minX)\n totalBounds.maxX = math.max(x2, totalBounds.maxX)\n totalBounds.minZ = math.min(z1, totalBounds.minZ)\n totalBounds.maxZ = math.max(z2, totalBounds.maxZ)\n end\n end\n\n -- default values (mainly for play area if nothing is found)\n if count == 0 then\n totalBounds.minX = -10\n totalBounds.maxX = -50\n totalBounds.minZ = -20\n totalBounds.maxZ = 20\n end\n\n totalBounds.middleX = (totalBounds.maxX + totalBounds.minX) / 2\n totalBounds.middleZ = (totalBounds.maxZ + totalBounds.minZ) / 2\n totalBounds.diffX = totalBounds.maxX - totalBounds.minX\n totalBounds.diffZ = totalBounds.maxZ - totalBounds.minZ\n\n return totalBounds\nend\n\nfunction loadCameraFromApi(params)\n loadCamera(params.player, params.camera)\nend\n\n-- loads the specified camera for a player\n---@param player tts__Player Player whose camera should be moved\n---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\nfunction loadCamera(player, camera)\n local lookHere, index, matColor\n local matColorList = { \"White\", \"Orange\", \"Green\", \"Red\" }\n local indexList = {\n White = 3,\n Orange = 4,\n Green = 5,\n Red = 6\n }\n\n if tonumber(camera) then\n index = tonumber(camera)\n matColor = matColorList[index - 2] -- mat index 1 - 4\n else\n index = indexList[camera]\n matColor = camera\n end\n\n -- dynamic view of the play area\n if index == 2 then\n -- search the scripting zone on the play area for objects\n local bounds = getDynamicViewBounds(getObjectFromGUID(\"a2f932\").getObjects())\n\n lookHere = {\n position = { bounds.middleX, 1.55, bounds.middleZ },\n yaw = 90,\n distance = 0.8 * math.max(bounds.diffX, bounds.diffZ) + 7\n }\n -- dynamic view of the clicked play mat\n elseif index \u003e= 3 and index \u003c= 6 then\n -- check if anyone (except for yourself) has claimed this color\n local isClaimed = false\n\n for playerColor, playerTable in pairs(claims) do\n if playerColor ~= player.color and playerTable[matColor] then\n isClaimed = true\n break\n end\n end\n\n -- swap to that color if it isn't claimed by someone else\n if #getSeatedPlayers() == 1 or not isClaimed then\n local newPlayerColor = playmatApi.getPlayerColor(matColor)\n copyVisibility({ startColor = player.color, targetColor = newPlayerColor })\n player.changeColor(newPlayerColor)\n player = Player[newPlayerColor]\n end\n\n -- search on the playmat for objects\n local bounds = getDynamicViewBounds(playmatApi.searchAroundPlaymat(matColor))\n\n lookHere = {\n position = { bounds.middleX, 0, bounds.middleZ },\n yaw = playmatApi.returnRotation(matColor).y + 180,\n distance = 0.42 * math.max(bounds.diffX, bounds.diffZ) + 7\n }\n end\n\n -- get default data if no dynamic view (play area or play mat) was loaded\n if not lookHere then\n lookHere = cameraData[index]\n lookHere.yaw = 90\n end\n\n -- set pitch to default if not edited\n lookHere.pitch = pitch[player.color] or 75\n\n -- update distance based on selected multiplier\n lookHere.distance = lookHere.distance * (distance[player.color] or 100) / 100\n\n -- delay is to account for colorswap\n Wait.frames(function() player.lookAt(lookHere) end, 2)\nend\n\n---------------------------------------------------------\n-- settings related functionality\n---------------------------------------------------------\n\n-- claims a color for a player\nfunction claimColor(player, color)\n local currentState = claims[player.color][color]\n claims[player.color][color] = not currentState\nend\n\nfunction loadDefaultSettings(player)\n -- reset claims for that player\n for _, color in ipairs(Player.getColors()) do\n claims[player.color][color] = (player.color == color)\n end\n\n -- reset pitch/distance for that player\n pitch[player.color] = nil\n distance[player.color] = nil\n\n -- update the UI accordingly\n updateSettingsUI(player)\nend\n\n-- called by clicking a toggle\nfunction toggleSettings(player)\n if settingsOpenForColor == player.color then\n settingsOpenForColor = nil\n UI.setAttribute(\"navPanelSettings\", \"active\", false)\n elseif settingsOpenForColor then\n broadcastToColor(\"Someone else is currently using the settings. Please wait and try again.\", player.color, \"Yellow\")\n else\n settingsOpenForColor = player.color\n\n updateSettingsUI(player)\n UI.setAttribute(\"navPanelSettings\", \"visibility\", player.color)\n UI.setAttribute(\"navPanelSettings\", \"active\", true)\n end\nend\n\n-- called by the navigation overlay options\nfunction updatePitch(player, number)\n pitch[player.color] = number\nend\n\n-- called by the navigation overlay options\nfunction updateDistance(player, number)\n distance[player.color] = number\nend\n\n-- updates the settings UI for the provided player\nfunction updateSettingsUI(player)\n -- update the sliders\n UI.setAttribute(\"sliderPitch\", \"value\", pitch[player.color] or 75)\n UI.setAttribute(\"sliderDistance\", \"value\", distance[player.color] or 100)\n \n -- update the claims\n local matColorList = { \"White\", \"Orange\", \"Green\", \"Red\" }\n for _, matColor in pairs(matColorList) do\n UI.setAttribute(\"claim\" .. matColor, \"isOn\", claims[player.color][matColor] or false)\n end\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/NavigationOverlayHandler\")\nend)\n__bundle_register(\"core/NavigationOverlayHandler\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\nfullButtonData = {\n { id = \"1\", width = \"84\", height = \"33\", offset = \"1 2\" }, -- 1. Act/Agenda\n { id = \"2\", width = \"78\", height = \"69\", offset = \"1 -62\" }, -- 2. Map\n { id = \"3\", width = \"70\", height = \"36\", offset = \"-38 -126\" }, -- 3. White\n { id = \"4\", width = \"70\", height = \"36\", offset = \"38 -126\" }, -- 4. Orange\n { id = \"5\", width = \"36\", height = \"70\", offset = \"-63 -66\" }, -- 5. Green\n { id = \"6\", width = \"36\", height = \"70\", offset = \"63 -66\" }, -- 6. Red\n { id = \"7\", width = \"38\", height = \"38\", offset = \"-65 -3\" }, -- 7. Victory\n { id = \"8\", width = \"40\", height = \"40\", offset = \"65 -3\" }, -- 8. Guide\n { id = \"9\", width = \"56\", height = \"16\", offset = \"1 -20\" }, -- 9. Player count\n { id = \"10\", width = \"36\", height = \"16\", offset = \"1 -102\" }, -- 10. Bless/Curse\n { id = \"11\", width = \"168\", height = \"56\", offset = \"1 47\" }, -- 11. Scenarios\n { id = \"12\", width = \"52\", height = \"53\", offset = \"-154 134\" }, -- 12. Player card panel\n { id = \"13\", width = \"22\", height = \"22\", offset = \"-116 132\" }, -- 13. Search card panel\n { id = \"14\", width = \"120\", height = \"75\", offset = \"-152 70\" }, -- 14. Player card display\n { id = \"15\", width = \"40\", height = \"54\", offset = \"-150 -38\" }, -- 15. Deck builder\n { id = \"16\", width = \"104\", height = \"84\", offset = \"-154 -114\" }, -- 16. Rules area\n { id = \"17\", width = \"100\", height = \"170\", offset = \"152 72\" }, -- 17. Cycle area\n { id = \"18\", width = \"56\", height = \"60\", offset = \"182 -124\" }, -- 18. Additions\n { id = \"19\", width = \"20\", height = \"20\", offset = \"0 150\" }, -- 19. Shrink\n { id = \"20\", width = \"20\", height = \"20\", offset = \"20 150\" }, -- 20. Close\n { id = \"21\", width = \"20\", height = \"20\", offset = \"-20 150\" } -- 21. Settings\n}\n\nplayButtonData = {\n { id = \"1\", width = \"80\", height = \"33\", offset = \"0 55\" },\n { id = \"2\", width = \"78\", height = \"70\", offset = \"0 -8\" },\n { id = \"3\", width = \"68\", height = \"32\", offset = \"-36 -71\" },\n { id = \"4\", width = \"68\", height = \"32\", offset = \"36 -71\" },\n { id = \"5\", width = \"35\", height = \"66\", offset = \"-65 -10\" },\n { id = \"6\", width = \"35\", height = \"66\", offset = \"65 -10\" },\n { id = \"7\", width = \"38\", height = \"38\", offset = \"-66 52\" },\n { id = \"8\", width = \"38\", height = \"38\", offset = \"66 52\" },\n { id = \"9\", width = \"50\", height = \"12\", offset = \"0 33\" },\n { id = \"10\", width = \"32\", height = \"12\", offset = \"0 -48\" },\n { id = \"19\", width = \"20\", height = \"20\", offset = \"0 80\" },\n { id = \"20\", width = \"20\", height = \"20\", offset = \"20 80\" },\n { id = \"21\", width = \"20\", height = \"20\", offset = \"-20 80\" }\n}\n\n-- To-Do: dynamically get positions by linking to objects\ncameraData = {\n { position = { -1.6, 1.55, 0 }, distance = 18 }, -- 1. Act/Agenda\n { position = { -28, 1.55, 0 }, distance = -1 }, -- 2. Map\n { position = { -31.6, 1.55, 26.4 }, distance = -1 }, -- 3. Green playermat\n { position = { -55, 1.55, 12.05 }, distance = -1 }, -- 4. White playermat\n { position = { -55, 1.55, -11.48 }, distance = -1 }, -- 5. Orange playermat\n { position = { -31.6, 1.55, -26.4 }, distance = -1 }, -- 6. Red playermat\n { position = { -3, 1.55, 30 }, distance = 16 }, -- 7. Victory / SetAside\n { position = { -3, 1.55, -26.76 }, distance = 16 }, -- 8. Guide\n { position = { -11.83, 1.55, 0 }, distance = 10 }, -- 9. Player count\n { position = { -48.35, 1.55, 0 }, distance = 10 }, -- 10. Bless/Curse\n { position = { 12.56, 1.55, 0 }, distance = 45 }, -- 11. Scenarios\n { position = { 57.8, 1.55, 71 }, distance = 22 }, -- 12. Player card panel\n { position = { 60.38, 1.55, 56 }, distance = 10 }, -- 13. Card search panel\n { position = { 27.48, 1.55, 71 }, distance = 35 }, -- 14. Player card area\n { position = { -19.48, 1.55, 71 }, distance = 22 }, -- 15. Deck builder\n { position = { -52.92, 1.55, 71 }, distance = 42 }, -- 16. Rules area\n { position = { 26, 1.55, -71 }, distance = 65 }, -- 17. Cycle area\n { position = { -59.08, 1.55, -83 }, distance = 27 } -- 18. Additions\n}\n\nlocal settingsOpenForColor\nlocal visibility = {}\nlocal claims = {}\nlocal pitch = {}\nlocal distance = {}\n\n---------------------------------------------------------\n-- save/load functionality\n---------------------------------------------------------\n\nfunction onSave()\n return JSON.encode({\n visibility = visibility,\n claims = claims,\n pitch = pitch,\n distance = distance\n })\nend\n\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n visibility = loadedData.visibility\n claims = loadedData.claims\n pitch = loadedData.pitch\n distance = loadedData.distance\n else\n -- initialize tables with defaults\n for _, color in ipairs(Player.getColors()) do\n claims[color] = {}\n visibility[color] = { full = false, play = false }\n end\n end\n\n createXmlButtons()\n updateVisibility()\nend\n\n---------------------------------------------------------\n-- visibility related functions\n---------------------------------------------------------\n\nfunction cycleVisibility(color)\n setVisibility(\"next\", color)\nend\n\nfunction copyVisibility(params)\n visibility[params.targetColor] = {\n full = visibility[params.startColor].full,\n play = visibility[params.startColor].play\n }\n updateVisibility()\nend\n\nfunction setVisibility(type, color)\n if type == \"next\" then\n if visibility[color].full then\n visibility[color] = { full = false, play = true }\n elseif visibility[color].play then\n visibility[color] = { full = false, play = false }\n else\n visibility[color] = { full = true, play = false }\n end\n elseif type == \"toggle\" then\n visibility[color] = {\n full = not visibility[color].full,\n play = not visibility[color].play\n }\n else\n visibility[color] = { full = false, play = false }\n end\n\n updateVisibility()\nend\n\n-- update XML visibility\nfunction updateVisibility()\n local colorString = { full = \"\", play = \"\" }\n\n for color, v in pairs(visibility) do\n if v.full then\n if colorString.full == \"\" then\n colorString.full = color\n else\n colorString.full = colorString.full .. '|' .. color\n end\n elseif v.play then\n if colorString.play == \"\" then\n colorString.play = color\n else\n colorString.play = colorString.play .. '|' .. color\n end\n end\n end\n\n -- update the visibility on the XML\n UI.setAttribute(\"navPanelFull\", \"visibility\", colorString.full)\n UI.setAttribute(\"navPanelPlay\", \"visibility\", colorString.play)\n UI.setAttribute(\"navPanelFull\", \"active\", colorString.full ~= \"\")\n UI.setAttribute(\"navPanelPlay\", \"active\", colorString.play ~= \"\")\nend\n\n---------------------------------------------------------\n-- XML button creation\n---------------------------------------------------------\n\nfunction createXmlButtons()\n local ui = UI.getXmlTable()\n ui = createXmlButtonHelper(ui, {\n data = fullButtonData,\n id = \"navPanelFull\",\n overlay = \"OverlayLarge\"\n })\n ui = createXmlButtonHelper(ui, {\n data = playButtonData,\n id = \"navPanelPlay\",\n overlay = \"OverlaySmall\"\n })\n UI.setXmlTable(ui)\nend\n\n-- XML button creation\nfunction createXmlButtonHelper(ui, params)\n local guid = self.getGUID()\n local xml = findTagWithId(ui, params.id)\n\n -- add basic image\n xml.children = { {\n tag = \"image\",\n attributes = {\n id = \"backgroundImage\",\n image = params.overlay\n }\n } }\n\n -- add all buttons\n for _, d in ipairs(params.data) do\n table.insert(xml.children, {\n tag = \"button\",\n attributes = {\n onClick = guid .. \"/buttonClicked\",\n id = d.id,\n height = d.height,\n width = d.width,\n offsetXY = d.offset,\n color = \"rgba(0,1,0,0)\"\n }\n })\n end\n return ui\nend\n\nfunction findTagWithId(ui, id)\n for _, obj in ipairs(ui) do\n if obj.attributes and obj.attributes.id and obj.attributes.id == id then return obj end\n if obj.children then\n local result = findTagWithId(obj.children, id)\n if result then return result end\n end\n end\n return nil\nend\n\n---------------------------------------------------------\n-- core functionality\n---------------------------------------------------------\n\n-- handles all button clicks\nfunction buttonClicked(player, _, id)\n local index = tonumber(id) or \"\"\n\n if index == 19 then\n setVisibility(\"toggle\", player.color)\n elseif index == 20 then\n setVisibility(\"close\", player.color)\n elseif index == 21 then\n toggleSettings(player)\n else\n loadCamera(player, index)\n end\nend\n\n-- generates a table with rectangular bounds for provided objects\nfunction getDynamicViewBounds(objList)\n local count = 0\n local totalBounds = {\n minX = 0,\n maxX = -70,\n minZ = 60,\n maxZ = -60\n }\n\n for _, obj in pairs(objList) do\n if not obj.hasTag(\"CameraZoom_ignore\") and not obj.hasTag(\"CampaignLog\") then\n count = count + 1\n local bounds = obj.getBounds()\n local x1 = bounds['center'][1] - bounds['size'][1] / 2\n local x2 = bounds['center'][1] + bounds['size'][1] / 2\n local z1 = bounds['center'][3] - bounds['size'][3] / 2\n local z2 = bounds['center'][3] + bounds['size'][3] / 2\n\n totalBounds.minX = math.min(x1, totalBounds.minX)\n totalBounds.maxX = math.max(x2, totalBounds.maxX)\n totalBounds.minZ = math.min(z1, totalBounds.minZ)\n totalBounds.maxZ = math.max(z2, totalBounds.maxZ)\n end\n end\n\n -- default values (mainly for play area if nothing is found)\n if count == 0 then\n totalBounds.minX = -10\n totalBounds.maxX = -50\n totalBounds.minZ = -20\n totalBounds.maxZ = 20\n end\n\n totalBounds.middleX = (totalBounds.maxX + totalBounds.minX) / 2\n totalBounds.middleZ = (totalBounds.maxZ + totalBounds.minZ) / 2\n totalBounds.diffX = totalBounds.maxX - totalBounds.minX\n totalBounds.diffZ = totalBounds.maxZ - totalBounds.minZ\n\n return totalBounds\nend\n\nfunction loadCameraFromApi(params)\n loadCamera(params.player, params.camera)\nend\n\n-- loads the specified camera for a player\n---@param player tts__Player Player whose camera should be moved\n---@param camera number|string If number: Index of the camera view to load | If string: Color of the playermat to swap to\nfunction loadCamera(player, camera)\n local lookHere, index, matColor\n local matColorList = { \"White\", \"Orange\", \"Green\", \"Red\" }\n local indexList = {\n White = 3,\n Orange = 4,\n Green = 5,\n Red = 6\n }\n\n if tonumber(camera) then\n index = tonumber(camera)\n matColor = matColorList[index - 2] -- mat index 1 - 4\n else\n index = indexList[camera]\n matColor = camera\n end\n\n -- dynamic view of the play area\n if index == 2 then\n -- search the scripting zone on the play area for objects\n local bounds = getDynamicViewBounds(getObjectFromGUID(\"a2f932\").getObjects())\n\n lookHere = {\n position = { bounds.middleX, 1.55, bounds.middleZ },\n yaw = 90,\n distance = 0.8 * math.max(bounds.diffX, bounds.diffZ) + 7\n }\n -- dynamic view of the clicked play mat\n elseif index \u003e= 3 and index \u003c= 6 then\n -- check if anyone (except for yourself) has claimed this color\n local isClaimed = false\n\n for playerColor, playerTable in pairs(claims) do\n if playerColor ~= player.color and playerTable[matColor] then\n isClaimed = true\n break\n end\n end\n\n -- swap to that color if it isn't claimed by someone else and it's currently unoccopied\n if #getSeatedPlayers() == 1 or (not isClaimed and isPlayermatAvailable(matColor)) then\n local newPlayerColor = playermatApi.getPlayerColor(matColor)\n copyVisibility({ startColor = player.color, targetColor = newPlayerColor })\n player.changeColor(newPlayerColor)\n player = Player[newPlayerColor]\n end\n\n -- search on the playermat for objects\n local bounds = getDynamicViewBounds(playermatApi.searchAroundPlayermat(matColor))\n\n lookHere = {\n position = { bounds.middleX, 0, bounds.middleZ },\n yaw = playermatApi.returnRotation(matColor).y + 180,\n distance = 0.42 * math.max(bounds.diffX, bounds.diffZ) + 7\n }\n end\n\n -- get default data if no dynamic view (play area or play mat) was loaded\n if not lookHere then\n lookHere = cameraData[index]\n lookHere.yaw = 90\n end\n\n -- set pitch to default if not edited\n lookHere.pitch = pitch[player.color] or 75\n\n -- update distance based on selected multiplier\n lookHere.distance = lookHere.distance * (distance[player.color] or 100) / 100\n\n -- delay is to account for colorswap\n Wait.frames(function() player.lookAt(lookHere) end, 2)\nend\n\n-- helper function to check if a playermat is available for a color swap\nfunction isPlayermatAvailable(matColor)\n local newPlayerColor = playermatApi.getPlayerColor(matColor)\n for _, color in ipairs(getSeatedPlayers()) do\n if color == newPlayerColor then\n return false\n end\n end\n return true\nend\n\n---------------------------------------------------------\n-- settings related functionality\n---------------------------------------------------------\n\n-- claims a color for a player\nfunction claimColor(player, color)\n local currentState = claims[player.color][color]\n claims[player.color][color] = not currentState\nend\n\nfunction loadDefaultSettings(player)\n -- reset claims for that player\n for _, color in ipairs(Player.getColors()) do\n claims[player.color][color] = (player.color == color)\n end\n\n -- reset pitch/distance for that player\n pitch[player.color] = nil\n distance[player.color] = nil\n\n -- update the UI accordingly\n updateSettingsUI(player)\nend\n\n-- called by clicking a toggle\nfunction toggleSettings(player)\n if settingsOpenForColor == player.color then\n settingsOpenForColor = nil\n UI.setAttribute(\"navPanelSettings\", \"active\", false)\n elseif settingsOpenForColor then\n broadcastToColor(\"Someone else is currently using the settings. Please wait and try again.\", player.color, \"Yellow\")\n else\n settingsOpenForColor = player.color\n\n updateSettingsUI(player)\n UI.setAttribute(\"navPanelSettings\", \"visibility\", player.color)\n UI.setAttribute(\"navPanelSettings\", \"active\", true)\n end\nend\n\n-- called by the navigation overlay options\nfunction updatePitch(player, number)\n pitch[player.color] = number\nend\n\n-- called by the navigation overlay options\nfunction updateDistance(player, number)\n distance[player.color] = number\nend\n\n-- updates the settings UI for the provided player\nfunction updateSettingsUI(player)\n -- update the sliders\n UI.setAttribute(\"sliderPitch\", \"value\", pitch[player.color] or 75)\n UI.setAttribute(\"sliderDistance\", \"value\", distance[player.color] or 100)\n \n -- update the claims\n local matColorList = { \"White\", \"Orange\", \"Green\", \"Red\" }\n for _, matColor in pairs(matColorList) do\n UI.setAttribute(\"claim\" .. matColor, \"isOn\", claims[player.color][matColor] or false)\n end\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "{\"claims\":{\"Black\":[],\"Blue\":[],\"Brown\":[],\"Green\":[],\"Grey\":[],\"Orange\":[],\"Pink\":[],\"Purple\":[],\"Red\":[],\"Teal\":[],\"White\":[],\"Yellow\":[]},\"distance\":[],\"pitch\":[],\"visibility\":{\"Black\":{\"full\":false,\"play\":false},\"Blue\":{\"full\":false,\"play\":false},\"Brown\":{\"full\":false,\"play\":false},\"Green\":{\"full\":false,\"play\":false},\"Grey\":{\"full\":false,\"play\":false},\"Orange\":{\"full\":false,\"play\":false},\"Pink\":{\"full\":false,\"play\":false},\"Purple\":{\"full\":false,\"play\":false},\"Red\":{\"full\":false,\"play\":false},\"Teal\":{\"full\":false,\"play\":false},\"White\":{\"full\":false,\"play\":false},\"Yellow\":{\"full\":false,\"play\":false}}}", "MeasureMovement": false, "Name": "go_game_piece_black", @@ -208805,7 +146255,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isActionToken = function(x) return x.getDescription() == \"Action Token\" end,\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filtering the result\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n pos = obj.getPosition()\n size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n direction = { 0, -1, 0 }\n maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\n__bundle_register(\"accessories/CampaignImporterExporter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal blessCurseApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckImporterApi = require(\"arkhamdb/DeckImporterApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playmatApi = require(\"playermat/PlaymatApi\")\n\n-- base data for token creation\nlocal campaignTokenData = {\n Name = \"Custom_Model_Bag\",\n Transform = {\n posX = -21.25,\n posY = 1.68,\n posZ = 55.59,\n rotX = 0,\n rotY = 270,\n rotZ = 0,\n scaleX = 2,\n scaleY = 2,\n scaleZ = 2\n },\n Description = \"SCED Importer Token\",\n Tags = { \"ImporterToken\" },\n CustomMesh = {\n MeshURL = \"http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/\",\n DiffuseURL = \"http://cloud-3.steamusercontent.com/ugc/254843371583188147/920981125E37B5CEB6C400E3FD353A2C428DA969/\",\n ColliderURL = \"http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/\",\n Convex = true,\n MaterialIndex = 2,\n TypeIndex = 6,\n CustomShader = {\n SpecularColor = {\n r = 0.72,\n g = 0.51,\n b = 0.34\n },\n SpecularIntensity = 0.4,\n SpecularSharpness = 7.0,\n FresnelStrength = 0.0\n }\n }\n}\n\nfunction onLoad()\n self.createButton({\n click_function = \"createCampaignToken\",\n function_owner = self,\n label = \"Export\",\n tooltip = \"Create a campaign save token!\",\n position = { x = -1, y = 0.21, z = 0 },\n font_size = 400,\n width = 1400,\n height = 600,\n scale = { 0.5, 1, 0.5 }\n })\nend\n\nfunction onObjectLeaveContainer(container)\n if container.hasTag(\"ImporterToken\") then\n broadcastToAll(\"Removing objects from the Save Coin bag will break functionality. Please return the removed objects.\", \"Yellow\")\n end\nend\n\nfunction onObjectEnterContainer(container)\n if container.hasTag(\"ImporterToken\") then\n broadcastToAll(\"Adding objects to the Save Coin bag will break functionality. Please remove the objects.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- main import functions (split up to allow for Wait conditions)\n---------------------------------------------------------\n\nfunction onCollisionEnter(info)\n if info.collision_object.hasTag(\"ImporterToken\") then\n importFromToken(info.collision_object)\n end\nend\n\n-- identifies import token, determines campaign box and downloads it (if needed)\nfunction importFromToken(coin)\n broadcastToAll(\"Campaign Import Initiated\")\n local importData = JSON.decode(coin.getGMNotes())\n local campaignBox = getObjectFromGUID(importData[\"box\"])\n\n if not campaignBox then\n broadcastToAll(\"Campaign Box not present on table!\", \"Red\")\n return\n end\n\n if campaignBox.type == \"Generic\" then\n campaignBox.call(\"buttonClick_download\")\n end\n\n Wait.condition(\n function()\n local campaignBox = getObjectFromGUID(importData[\"box\"])\n if #campaignBox.getObjects() \u003e 0 then\n placeCampaignFromToken(importData, coin)\n else\n restoreCampaignData(importData, coin)\n end\n end,\n function()\n local obj = getObjectFromGUID(importData[\"box\"])\n if obj == nil then\n return false\n else\n return obj.type == \"Bag\"\n end\n end,\n 2,\n function() broadcastToAll(\"Error loading campaign box\") end\n )\nend\n\n-- after box has been downloaded, places content on table\nfunction placeCampaignFromToken(importData, coin)\n getObjectFromGUID(importData[\"box\"]).call(\"buttonClick_place\")\n Wait.condition(\n function() restoreCampaignData(importData, coin) end,\n function() return findUniqueObjectWithTag(\"CampaignLog\") ~= nil end,\n 2,\n function() broadcastToAll(\"Error placing campaign box\") end\n )\nend\n\n-- after content is placed on table, conducts all the other import operations\nfunction restoreCampaignData(importData, coin)\n -- go over internal items and respawn them (only storing campaign log and additional player cards)\n for _, objData in ipairs(coin.getData().ContainedObjects) do\n objData.Locked = true\n local spawnData = { data = objData }\n\n -- maybe restore position of item and destroy duplicate\n if objData.Nickname == \"Additional Player Cards\" then\n local additionalIndex = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AdditionalPlayerCardsBag\")\n spawnData.position = additionalIndex.getPosition()\n additionalIndex.destruct()\n else\n local campaignLog = findUniqueObjectWithTag(\"CampaignLog\")\n if campaignLog then\n spawnData.position = campaignLog.getPosition()\n campaignLog.destruct()\n end\n end\n\n spawnObjectData(spawnData)\n end\n\n chaosBagApi.setChaosBagState(importData[\"bag\"])\n\n -- populate trauma values\n if importData[\"trauma\"] then\n setTrauma(importData[\"trauma\"])\n end\n\n -- populate ArkhamDB deck IDs\n if importData[\"decks\"] then\n deckImporterApi.setUiState(importData[\"decks\"])\n end\n\n -- maybe set campaign guide page\n if importData[\"guide\"] then\n local campaignGuide = findUniqueObjectWithTag(\"CampaignGuide\")\n if campaignGuide then\n Wait.condition(\n -- Called after the condition function returns true\n function() printToAll(\"Campaign Guide import successful!\") end,\n -- Condition function that is called continuously until it returns true or timeout is reached\n function() return campaignGuide.Book.setPage(importData[\"guide\"]) end,\n -- Amount of time in seconds until the Wait times out\n 2,\n -- Called if the Wait times out\n function() printToAll(\"Campaign Guide import failed!\") end\n )\n end\n end\n\n Wait.time(function() optionPanelApi.loadSettings(importData[\"options\"]) end, 0.5)\n\n -- destroy Tour Starter token\n local tourStarter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TourStarter\")\n if tourStarter then\n tourStarter.destruct()\n end\n\n -- restore PlayArea image and player count\n playAreaApi.updateSurface(importData[\"playarea\"])\n playAreaApi.setInvestigatorCount(importData[\"clueCount\"])\n\n coin.destruct()\n broadcastToAll(\"Campaign successfully imported!\", \"Green\")\nend\n\n-- creates a campaign token with save data encoded into GM Notes based on the current state of the table\nfunction createCampaignToken(_, playerColor, _)\n local campaignData = {}\n\n -- need to reset the contained objects to support multiple exports\n campaignTokenData.ContainedObjects = {}\n\n -- find active campaign\n local campaignBox\n for _, obj in ipairs(getObjectsWithTag(\"CampaignBox\")) do\n if obj.type == \"Bag\" and #obj.getObjects() == 0 then\n if not campaignBox then\n campaignBox = obj\n else\n broadcastToAll(\"Multiple empty campaign boxes detected; delete all but one.\", \"Red\")\n return\n end\n end\n end\n\n if not campaignBox then\n broadcastToAll(\"Campaign box with all placed objects not found!\", \"Red\")\n return\n end\n\n -- clean up chaos tokens (needs to happen before saving chaos bag state)\n blessCurseApi.removeAll(playerColor)\n chaosBagApi.releaseAllSealedTokens(playerColor)\n\n -- main data collection\n campaignData.box = campaignBox.getGUID()\n campaignData.bag = chaosBagApi.getChaosBagState()\n campaignData.decks = deckImporterApi.getUiState()\n campaignData.clueCount = playAreaApi.getInvestigatorCount()\n campaignData.playarea = playAreaApi.getSurface()\n campaignData.options = optionPanelApi.getOptions()\n\n -- save campaign log if present\n local campaignLog = findUniqueObjectWithTag(\"CampaignLog\")\n if campaignLog then\n local logData = campaignLog.getData()\n logData.Locked = false\n table.insert(campaignTokenData.ContainedObjects, logData)\n\n -- maybe also extract the trauma values\n local trauma = campaignLog.getVar(\"returnTrauma\")\n if trauma then\n printToAll(\"Trauma values found in campaign log!\", \"Green\")\n campaignData.trauma = {}\n for _, val in ipairs(campaignLog.call(\"returnTrauma\")) do\n table.insert(campaignData.trauma, val)\n end\n else\n printToAll(\"Trauma values could not be found in campaign log!\", \"Yellow\")\n end\n end\n\n -- store campaign guide page if present\n local campaignGuide = findUniqueObjectWithTag(\"CampaignGuide\")\n if campaignGuide then\n campaignData.guide = campaignGuide.Book.getPage()\n end\n\n -- store the additional index if there are any cards in it\n local additionalIndex = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AdditionalPlayerCardsBag\")\n if additionalIndex and #additionalIndex.getObjects() \u003e 0 then\n local indexData = additionalIndex.getData()\n indexData.Locked = false\n table.insert(campaignTokenData.ContainedObjects, indexData)\n end\n\n -- finish the data for the campaign token\n campaignTokenData.GMNotes = JSON.encode(campaignData)\n campaignTokenData.Nickname = campaignBox.getName() .. os.date(\" %b %d\") .. \" Save\"\n\n spawnObjectData({ data = campaignTokenData })\n broadcastToAll(\"Campaign successfully exported! Save coin object to import on a different save.\", \"Green\")\nend\n\n---------------------------------------------------------\n-- helper functions\n---------------------------------------------------------\n\nfunction findUniqueObjectWithTag(tag)\n local objects = getObjectsWithTag(tag)\n if not objects then return end\n\n if #objects == 1 then\n return objects[1]\n elseif #objects == 0 then\n broadcastToAll(\"No \" .. tag .. \" detected; ensure it has the correct tag.\", \"Red\")\n else\n broadcastToAll(\"More than one \" .. tag .. \" detected; delete all but one.\", \"Red\")\n end\nend\n\nfunction setTrauma(trauma)\n for i, matColor in ipairs({ \"White\", \"Orange\", \"Green\", \"Red\" }) do\n playmatApi.updateCounter(matColor, \"DamageCounter\", trauma[i])\n playmatApi.updateCounter(matColor, \"HorrorCounter\", trauma[i + 4])\n end\nend\nend)\n__bundle_register(\"arkhamdb/DeckImporterApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckImporterApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getDeckImporter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DeckImporter\")\n end\n\n ---@class uiStateTable\n ---@field redDeck string Deck ID to load for the red player\n ---@field orangeDeck string Deck ID to load for the orange player\n ---@field whiteDeck string Deck ID to load for the white player\n ---@field greenDeck string Deck ID to load for the green player\n ---@field privateDeck boolean True to load a private deck, false to load a public deck\n ---@field loadNewest boolean True if the most upgraded version of the deck should be loaded\n ---@field investigators boolean True if investigator cards should be spawned\n\n -- Returns a table with the full state of the UI, including options and deck IDs.\n -- This can be used to persist via onSave(), or provide values for a load operation\n ---@return uiStateTable uiStateTable Contains data about the current UI state\n DeckImporterApi.getUiState = function()\n local passthroughTable = {}\n for k,v in pairs(getDeckImporter().call(\"getUiState\")) do\n passthroughTable[k] = v\n end\n return passthroughTable\n end\n\n -- Updates the state of the UI based on the provided table. Any values not provided will be left the same.\n ---@return uiStateTable uiStateTable Contains data about the current UI state\n DeckImporterApi.setUiState = function(uiStateTable)\n return getDeckImporter().call(\"setUiState\", uiStateTable)\n end\n\n return DeckImporterApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.releasedToken = function(type, guid)\n getManager().call(\"releasedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"playermat/PlaymatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlaymatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playmat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playmat\n ---@param startPos table Starting position to get the closest mat from\n PlaymatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playmat that owns the playercolor's hand\n ---@param handColor string Color of the playmat\n PlaymatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.isDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"isDES\")\n end\n end\n\n -- Performs a search of the deck area of the requested playmat and returns the result as table\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlaymatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlaymatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playmat for the provided list of objects\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlaymatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlaymatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlaymatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playmat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playmat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playmat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playmat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlaymatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlaymatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playmat\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlaymatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlaymatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlaymatApi.getUsedMatColors = function()\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Finds all objects on the playmat and associated set aside zone and returns a table\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlaymatApi.searchAroundPlaymat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playmat - White, Orange, Green, Red or All\n PlaymatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playmats\n PlaymatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlaymatApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/CampaignImporterExporter\")\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playmat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playmat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playmat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\")) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/CampaignImporterExporter\")\nend)\n__bundle_register(\"accessories/CampaignImporterExporter\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal blessCurseApi = require(\"chaosbag/BlessCurseManagerApi\")\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal deckImporterApi = require(\"arkhamdb/DeckImporterApi\")\nlocal guidReferenceApi = require(\"core/GUIDReferenceApi\")\nlocal optionPanelApi = require(\"core/OptionPanelApi\")\nlocal playAreaApi = require(\"core/PlayAreaApi\")\nlocal playermatApi = require(\"playermat/PlayermatApi\")\n\n-- base data for token creation\nlocal campaignTokenData = {\n Name = \"Custom_Model_Bag\",\n Transform = {\n posX = -21.25,\n posY = 1.68,\n posZ = 55.59,\n rotX = 0,\n rotY = 270,\n rotZ = 0,\n scaleX = 2,\n scaleY = 2,\n scaleZ = 2\n },\n Description = \"SCED Importer Token\",\n Tags = { \"ImporterToken\" },\n CustomMesh = {\n MeshURL = \"http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/\",\n DiffuseURL = \"http://cloud-3.steamusercontent.com/ugc/254843371583188147/920981125E37B5CEB6C400E3FD353A2C428DA969/\",\n ColliderURL = \"http://cloud-3.steamusercontent.com/ugc/943949966265929204/A38BB5D72419E6298385556D931877C0A1A55C17/\",\n Convex = true,\n MaterialIndex = 2,\n TypeIndex = 6,\n CustomShader = {\n SpecularColor = {\n r = 0.72,\n g = 0.51,\n b = 0.34\n },\n SpecularIntensity = 0.4,\n SpecularSharpness = 7.0,\n FresnelStrength = 0.0\n }\n }\n}\n\nfunction onLoad()\n self.createButton({\n click_function = \"createCampaignToken\",\n function_owner = self,\n label = \"Export\",\n tooltip = \"Create a campaign save token!\",\n position = { x = -1, y = 0.21, z = 0 },\n font_size = 400,\n width = 1400,\n height = 600,\n scale = { 0.5, 1, 0.5 }\n })\nend\n\nfunction onObjectLeaveContainer(container)\n if container.hasTag(\"ImporterToken\") then\n broadcastToAll(\"Removing objects from the Save Coin bag will break functionality. Please return the removed objects.\", \"Yellow\")\n end\nend\n\nfunction onObjectEnterContainer(container)\n if container.hasTag(\"ImporterToken\") then\n broadcastToAll(\"Adding objects to the Save Coin bag will break functionality. Please remove the objects.\", \"Yellow\")\n end\nend\n\n---------------------------------------------------------\n-- main import functions (split up to allow for Wait conditions)\n---------------------------------------------------------\n\nfunction onCollisionEnter(info)\n if info.collision_object.hasTag(\"ImporterToken\") then\n importFromToken(info.collision_object)\n end\nend\n\n-- identifies import token, determines campaign box and downloads it (if needed)\nfunction importFromToken(coin)\n broadcastToAll(\"Campaign Import Initiated\")\n local importData = JSON.decode(coin.getGMNotes())\n local campaignBox = getObjectFromGUID(importData[\"box\"])\n\n if not campaignBox then\n broadcastToAll(\"Campaign Box not present on table!\", \"Red\")\n return\n end\n\n if campaignBox.type == \"Generic\" then\n campaignBox.call(\"buttonClick_download\")\n end\n\n Wait.condition(\n function()\n campaignBox = getObjectFromGUID(importData[\"box\"])\n if #campaignBox.getObjects() \u003e 0 then\n placeCampaignFromToken(importData, coin)\n else\n restoreCampaignData(importData, coin)\n end\n end,\n function()\n campaignBox = getObjectFromGUID(importData[\"box\"])\n if campaignBox == nil then\n return false\n else\n return campaignBox.type == \"Bag\"\n end\n end,\n 2,\n function() broadcastToAll(\"Error loading campaign box\") end\n )\nend\n\n-- after box has been downloaded, places content on table\nfunction placeCampaignFromToken(importData, coin)\n getObjectFromGUID(importData[\"box\"]).call(\"buttonClick_place\")\n Wait.condition(\n function() restoreCampaignData(importData, coin) end,\n function() return findUniqueObjectWithTag(\"CampaignLog\") ~= nil end,\n 2,\n function() broadcastToAll(\"Error placing campaign box\") end\n )\nend\n\n-- after content is placed on table, conducts all the other import operations\nfunction restoreCampaignData(importData, coin)\n -- go over internal items and respawn them (only storing campaign log and additional player cards)\n for _, objData in ipairs(coin.getData().ContainedObjects) do\n objData.Locked = true\n local spawnData = { data = objData }\n\n -- maybe restore position of item and destroy duplicate\n if objData.Nickname == \"Additional Player Cards\" then\n local additionalIndex = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AdditionalPlayerCardsBag\")\n spawnData.position = additionalIndex.getPosition()\n additionalIndex.destruct()\n else\n local campaignLog = findUniqueObjectWithTag(\"CampaignLog\")\n if campaignLog then\n spawnData.position = campaignLog.getPosition()\n campaignLog.destruct()\n end\n end\n\n spawnObjectData(spawnData)\n end\n\n chaosBagApi.setChaosBagState(importData[\"bag\"])\n\n -- populate trauma values\n if importData[\"trauma\"] then\n setTrauma(importData[\"trauma\"])\n end\n\n -- populate ArkhamDB deck IDs\n if importData[\"decks\"] then\n deckImporterApi.setUiState(importData[\"decks\"])\n end\n\n -- maybe set campaign guide page (unless it was on the first page)\n if importData[\"guide\"] and importData[\"guide\"] ~= 0 then\n local campaignGuide = findUniqueObjectWithTag(\"CampaignGuide\")\n if campaignGuide then\n Wait.condition(\n -- Called after the condition function returns true\n function() printToAll(\"Campaign Guide import successful!\") end,\n -- Condition function that is called continuously until it returns true or timeout is reached\n function() return campaignGuide.Book.setPage(importData[\"guide\"]) end,\n -- Amount of time in seconds until the Wait times out\n 2,\n -- Called if the Wait times out\n function() printToAll(\"Campaign Guide import failed!\") end\n )\n end\n end\n\n Wait.time(function() optionPanelApi.loadSettings(importData[\"options\"]) end, 0.5)\n\n -- destroy Tour Starter token\n local tourStarter = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"TourStarter\")\n if tourStarter then\n tourStarter.destruct()\n end\n\n -- restore PlayArea image and player count\n playAreaApi.updateSurface(importData[\"playarea\"])\n playAreaApi.setInvestigatorCount(importData[\"clueCount\"])\n\n -- restore playermat slots\n if importData[\"slotData\"] then\n for matColor, slotData in pairs(importData[\"slotData\"]) do\n playermatApi.loadSlotData(matColor, slotData)\n end\n end\n\n coin.destruct()\n broadcastToAll(\"Campaign successfully imported!\", \"Green\")\nend\n\n-- creates a campaign token with save data encoded into GM Notes based on the current state of the table\nfunction createCampaignToken(_, playerColor, _)\n local campaignData = {}\n\n -- need to reset the contained objects to support multiple exports\n campaignTokenData.ContainedObjects = {}\n\n -- find active campaign\n local campaignBox\n for _, obj in ipairs(getObjectsWithTag(\"CampaignBox\")) do\n if obj.type == \"Bag\" and #obj.getObjects() == 0 then\n if not campaignBox then\n campaignBox = obj\n else\n broadcastToAll(\"Multiple empty campaign boxes detected; delete all but one.\", \"Red\")\n return\n end\n end\n end\n\n if not campaignBox then\n broadcastToAll(\"Campaign box with all placed objects not found!\", \"Red\")\n return\n end\n\n -- clean up chaos tokens (needs to happen before saving chaos bag state)\n blessCurseApi.removeAll(playerColor)\n chaosBagApi.releaseAllSealedTokens(playerColor)\n\n -- main data collection\n campaignData.box = campaignBox.getGUID()\n campaignData.bag = chaosBagApi.getChaosBagState()\n campaignData.decks = deckImporterApi.getUiState()\n campaignData.clueCount = playAreaApi.getInvestigatorCount()\n campaignData.playarea = playAreaApi.getSurface()\n campaignData.options = optionPanelApi.getOptions()\n\n -- save campaign log if present\n local campaignLog = findUniqueObjectWithTag(\"CampaignLog\")\n if campaignLog then\n local logData = campaignLog.getData()\n logData.Locked = false\n table.insert(campaignTokenData.ContainedObjects, logData)\n\n -- maybe also extract the trauma values\n local trauma = campaignLog.getVar(\"returnTrauma\")\n if trauma then\n printToAll(\"Trauma values found in campaign log!\", \"Green\")\n campaignData.trauma = {}\n for _, val in ipairs(campaignLog.call(\"returnTrauma\")) do\n table.insert(campaignData.trauma, val)\n end\n else\n printToAll(\"Trauma values could not be found in campaign log!\", \"Yellow\")\n end\n end\n\n -- store campaign guide page if present\n local campaignGuide = findUniqueObjectWithTag(\"CampaignGuide\")\n if campaignGuide then\n campaignData.guide = campaignGuide.Book.getPage()\n end\n\n -- store the additional index if there are any cards in it\n local additionalIndex = guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"AdditionalPlayerCardsBag\")\n if additionalIndex and #additionalIndex.getObjects() \u003e 0 then\n local indexData = additionalIndex.getData()\n indexData.Locked = false\n table.insert(campaignTokenData.ContainedObjects, indexData)\n end\n\n -- get the slot symbol data for each playermat (use GUIDReferenceApi to only get this for existing playermats)\n campaignData.slotData = {}\n for matColor, _ in pairs(guidReferenceApi.getObjectsByType(\"Playermat\")) do\n local slotData = playermatApi.getSlotData(matColor)\n campaignData.slotData[matColor] = slotData\n end\n\n -- finish the data for the campaign token\n campaignTokenData.GMNotes = JSON.encode(campaignData)\n campaignTokenData.Nickname = campaignBox.getName() .. os.date(\" %b %d\") .. \" Save\"\n\n spawnObjectData({ data = campaignTokenData })\n broadcastToAll(\"Campaign successfully exported! Save coin object to import on a different save.\", \"Green\")\nend\n\n---------------------------------------------------------\n-- helper functions\n---------------------------------------------------------\n\nfunction findUniqueObjectWithTag(tag)\n local objects = getObjectsWithTag(tag)\n if not objects then return end\n\n if #objects == 1 then\n return objects[1]\n elseif #objects == 0 then\n broadcastToAll(\"No \" .. tag .. \" detected; ensure it has the correct tag.\", \"Red\")\n else\n broadcastToAll(\"More than one \" .. tag .. \" detected; delete all but one.\", \"Red\")\n end\nend\n\nfunction setTrauma(trauma)\n for i, matColor in ipairs({ \"White\", \"Orange\", \"Green\", \"Red\" }) do\n playermatApi.updateCounter(matColor, \"DamageCounter\", trauma[i])\n playermatApi.updateCounter(matColor, \"HorrorCounter\", trauma[i + 4])\n end\nend\nend)\n__bundle_register(\"arkhamdb/DeckImporterApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local DeckImporterApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getDeckImporter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"DeckImporter\")\n end\n\n ---@class uiStateTable\n ---@field redDeck string Deck ID to load for the red player\n ---@field orangeDeck string Deck ID to load for the orange player\n ---@field whiteDeck string Deck ID to load for the white player\n ---@field greenDeck string Deck ID to load for the green player\n ---@field privateDeck boolean True to load a private deck, false to load a public deck\n ---@field loadNewest boolean True if the most upgraded version of the deck should be loaded\n ---@field investigators boolean True if investigator cards should be spawned\n\n -- Returns a table with the full state of the UI, including options and deck IDs.\n -- This can be used to persist via onSave(), or provide values for a load operation\n ---@return uiStateTable uiStateTable Contains data about the current UI state\n DeckImporterApi.getUiState = function()\n local passthroughTable = {}\n for k,v in pairs(getDeckImporter().call(\"getUiState\")) do\n passthroughTable[k] = v\n end\n return passthroughTable\n end\n\n -- Updates the state of the UI based on the provided table. Any values not provided will be left the same.\n ---@return uiStateTable uiStateTable Contains data about the current UI state\n DeckImporterApi.setUiState = function(uiStateTable)\n return getDeckImporter().call(\"setUiState\", uiStateTable)\n end\n\n return DeckImporterApi\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/PlayAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getPlayArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"PlayArea\")\n end\n\n local function getInvestigatorCounter()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"InvestigatorCounter\")\n end\n\n -- Returns the current value of the investigator counter from the playermat\n ---@return number: Number of investigators currently set on the counter\n PlayAreaApi.getInvestigatorCount = function()\n return getInvestigatorCounter().getVar(\"val\")\n end\n\n -- Updates the current value of the investigator counter from the playermat\n ---@param count number Number of investigators to set on the counter\n PlayAreaApi.setInvestigatorCount = function(count)\n getInvestigatorCounter().call(\"updateVal\", count)\n end\n\n -- Move all contents on the play area (cards, tokens, etc) one slot in the given direction. Certain\n -- fixed objects will be ignored, as will anything the player has tagged with 'displacement_excluded'\n ---@param playerColor string Color of the player requesting the shift for messages\n PlayAreaApi.shiftContentsUp = function(playerColor)\n getPlayArea().call(\"shiftContentsUp\", playerColor)\n end\n\n PlayAreaApi.shiftContentsDown = function(playerColor)\n getPlayArea().call(\"shiftContentsDown\", playerColor)\n end\n\n PlayAreaApi.shiftContentsLeft = function(playerColor)\n getPlayArea().call(\"shiftContentsLeft\", playerColor)\n end\n\n PlayAreaApi.shiftContentsRight = function(playerColor)\n getPlayArea().call(\"shiftContentsRight\", playerColor)\n end\n\n ---@param state boolean This controls whether location connections should be drawn\n PlayAreaApi.setConnectionDrawState = function(state)\n getPlayArea().call(\"setConnectionDrawState\", state)\n end\n\n ---@param color string Connection color to be used for location connections\n PlayAreaApi.setConnectionColor = function(color)\n getPlayArea().call(\"setConnectionColor\", color)\n end\n\n -- Event to be called when the current scenario has changed\n ---@param scenarioName string Name of the new scenario\n PlayAreaApi.onScenarioChanged = function(scenarioName)\n getPlayArea().call(\"onScenarioChanged\", scenarioName)\n end\n\n -- Sets this playermat's snap points to limit snapping to locations or not.\n -- If matchTypes is false, snap points will be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n PlayAreaApi.setLimitSnapsByType = function(matchCardTypes)\n getPlayArea().call(\"setLimitSnapsByType\", matchCardTypes)\n end\n\n -- Receiver for the Global tryObjectEnterContainer event. Used to clear vector lines from dragged\n -- cards before they're destroyed by entering the container\n PlayAreaApi.tryObjectEnterContainer = function(container, object)\n getPlayArea().call(\"tryObjectEnterContainer\", { container = container, object = object })\n end\n\n -- Counts the VP on locations in the play area\n PlayAreaApi.countVP = function()\n return getPlayArea().call(\"countVP\")\n end\n\n -- Highlights all locations in the play area without metadata\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightMissingData = function(state)\n return getPlayArea().call(\"highlightMissingData\", state)\n end\n\n -- Highlights all locations in the play area with VP\n ---@param state boolean True if highlighting should be enabled\n PlayAreaApi.highlightCountedVP = function(state)\n return getPlayArea().call(\"countVP\", state)\n end\n\n -- Checks if an object is in the play area (returns true or false)\n PlayAreaApi.isInPlayArea = function(object)\n return getPlayArea().call(\"isInPlayArea\", object)\n end\n\n -- Returns the current surface of the play area\n PlayAreaApi.getSurface = function()\n return getPlayArea().getCustomObject().image\n end\n\n -- Updates the surface of the play area\n PlayAreaApi.updateSurface = function(url)\n return getPlayArea().call(\"updateSurface\", url)\n end\n\n -- Returns a deep copy of the currently tracked locations\n PlayAreaApi.getTrackedLocations = function()\n local t = {}\n for k, v in pairs(getPlayArea().call(\"getTrackedLocations\", {})) do\n t[k] = v\n end\n return t\n end\n\n -- Called by Custom Data Helpers to push their location data into the Data Helper. This adds the\n -- data to the local token manager instance.\n ---@param args table Single-value array holding the GUID of the Custom Data Helper making the call\n PlayAreaApi.updateLocations = function(args)\n getPlayArea().call(\"updateLocations\", args)\n end\n\n PlayAreaApi.getCustomDataHelper = function()\n return getPlayArea().getVar(\"customDataHelper\")\n end\n\n return PlayAreaApi\nend\nend)\n__bundle_register(\"playermat/PlayermatApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local PlayermatApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n local searchLib = require(\"util/SearchLib\")\n local localInvestigatorPosition = { x = -1.17, y = 1, z = -0.01 }\n\n -- Convenience function to look up a mat's object by color, or get all mats.\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@return table: Single-element if only single playermat is requested\n local function getMatForColor(matColor)\n if matColor == \"All\" then\n return guidReferenceApi.getObjectsByType(\"Playermat\")\n else\n return { matColor = guidReferenceApi.getObjectByOwnerAndType(matColor, \"Playermat\") }\n end\n end\n\n -- Returns the color of the closest playermat\n ---@param startPos table Starting position to get the closest mat from\n PlayermatApi.getMatColorByPosition = function(startPos)\n local result, smallestDistance\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local distance = Vector.between(startPos, mat.getPosition()):magnitude()\n if smallestDistance == nil or distance \u003c smallestDistance then\n smallestDistance = distance\n result = matColor\n end\n end\n return result\n end\n\n -- Returns the color of the player's hand that is seated next to the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getPlayerColor = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"playerColor\")\n end\n end\n\n -- Returns the color of the playermat that owns the playercolor's hand\n ---@param handColor string Color of the playermat\n PlayermatApi.getMatColor = function(handColor)\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local playerColor = mat.getVar(\"playerColor\")\n if playerColor == handColor then\n return matColor\n end\n end\n end\n\n -- Instructs a playermat to check for DES\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.checkForDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"checkForDES\")\n end\n end\n\n -- Returns if there is the card \"Dream-Enhancing Serum\" on the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@return boolean: whether DES is present on the playermat\n PlayermatApi.hasDES = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"hasDES\")\n end\n end\n\n -- gets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getSlotData = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getTable(\"slotData\")\n end\n end\n\n -- sets the slot data for the playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param newSlotData table New slot data for the playermat\n PlayermatApi.loadSlotData = function(matColor, newSlotData)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.setTable(\"slotData\", newSlotData)\n mat.call(\"redrawSlotSymbols\")\n return\n end\n end\n\n -- Performs a search of the deck area of the requested playermat and returns the result as table\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDeckAreaObjects = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getDeckAreaObjects\")\n end\n end\n\n -- Flips the top card of the deck (useful after deck manipulation for Norman Withers)\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.flipTopCardFromDeck = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"flipTopCardFromDeck\")\n end\n end\n\n -- Returns the position of the discard pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDiscardPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDiscardPosition\")\n end\n end\n\n -- Returns the position of the draw pile of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getDrawPosition = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"returnGlobalDrawPosition\")\n end\n end\n\n -- Transforms a local position into a global position\n ---@param localPos table Local position to be transformed\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.transformLocalPosition = function(localPos, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.positionToWorld(localPos)\n end\n end\n\n -- Returns the rotation of the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnRotation = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getRotation()\n end\n end\n\n -- Returns a table with spawn data (position and rotation) for a helper object\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param helperName string Name of the helper object\n PlayermatApi.getHelperSpawnData = function(matColor, helperName)\n local resultTable = {}\n local localPositionTable = {\n [\"Hand Helper\"] = {0.05, 0, -1.182},\n [\"Search Assistant\"] = {-0.3, 0, -1.182}\n }\n \n for color, mat in pairs(getMatForColor(matColor)) do\n resultTable[color] = {\n position = mat.positionToWorld(localPositionTable[helperName]),\n rotation = mat.getRotation()\n }\n end\n return resultTable\n end\n\n\n -- Triggers the Upkeep for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param playerColor string Color of the calling player (for messages)\n PlayermatApi.doUpkeepFromHotkey = function(matColor, playerColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doUpkeepFromHotkey\", playerColor)\n end\n end\n\n -- Handles discarding for the requested playermat for the provided list of objects\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param objList table List of objects to discard\n PlayermatApi.discardListOfObjects = function(matColor, objList)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"discardListOfObjects\", objList)\n end\n end\n\n -- Returns the active investigator id\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorId = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorId\")\n end\n end\n\n -- Returns the class of the active investigator\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.returnInvestigatorClass = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.getVar(\"activeInvestigatorClass\")\n end\n end\n\n -- Returns the position for encounter card drawing\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param stack boolean If true, returns the leftmost position instead of the first empty from the right\n PlayermatApi.getEncounterCardDrawPosition = function(matColor, stack)\n for _, mat in pairs(getMatForColor(matColor)) do\n return Vector(mat.call(\"getEncounterCardDrawPosition\", stack))\n end\n end\n\n -- Sets the requested playermat's snap points to limit snapping to matching card types or not. If\n -- matchTypes is true, the main card slot snap points will only snap assets, while the\n -- investigator area point will only snap Investigators. If matchTypes is false, snap points will\n -- be reset to snap all cards.\n ---@param matchCardTypes boolean Whether snap points should only snap for the matching card types\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.setLimitSnapsByType = function(matchCardTypes, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"setLimitSnapsByType\", matchCardTypes)\n end\n end\n\n -- Sets the requested playermat's draw 1 button to visible\n ---@param isDrawButtonVisible boolean Whether the draw 1 button should be visible or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.showDrawButton = function(isDrawButtonVisible, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"showDrawButton\", isDrawButtonVisible)\n end\n end\n\n -- Shows or hides the clickable clue counter for the requested playermat\n ---@param showCounter boolean Whether the clickable counter should be present or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.clickableClues = function(showCounter, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"clickableClues\", showCounter)\n end\n end\n\n -- Toggles the use of class textures for the requested playermat\n ---@param state boolean Whether the class texture should be used or not\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.useClassTexture = function(state, matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"useClassTexture\", state)\n end\n end\n\n -- Removes all clues (to the trash for tokens and counters set to 0) for the requested playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.removeClues = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"removeClues\")\n end\n end\n\n -- Reports the clue count for the requested playermat\n ---@param useClickableCounters boolean Controls which type of counter is getting checked\n PlayermatApi.getClueCount = function(useClickableCounters, matColor)\n local count = 0\n for _, mat in pairs(getMatForColor(matColor)) do\n count = count + mat.call(\"getClueCount\", useClickableCounters)\n end\n return count\n end\n\n -- Updates the specified owned counter\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param type string Counter to target\n ---@param newValue number Value to set the counter to\n ---@param modifier number If newValue is not provided, the existing value will be adjusted by this modifier\n PlayermatApi.updateCounter = function(matColor, type, newValue, modifier)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"updateCounter\", { type = type, newValue = newValue, modifier = modifier })\n end\n end\n\n -- Triggers the draw function for the specified playermat\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param number number Amount of cards to draw\n PlayermatApi.drawCardsWithReshuffle = function(matColor, number)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"drawCardsWithReshuffle\", number)\n end\n end\n\n -- Returns the resource counter amount\n ---@param matColor string Color of the playermat - White, Orange, Green or Red (does not support \"All\")\n ---@param type string Counter to target\n PlayermatApi.getCounterValue = function(matColor, type)\n for _, mat in pairs(getMatForColor(matColor)) do\n return mat.call(\"getCounterValue\", type)\n end\n end\n\n -- Returns a list of mat colors that have an investigator placed\n PlayermatApi.getUsedMatColors = function()\n local usedColors = {}\n for matColor, mat in pairs(getMatForColor(\"All\")) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult \u003e 0 then\n table.insert(usedColors, matColor)\n end\n end\n return usedColors\n end\n\n -- Returns investigator name\n ---@param matColor string Color of the playmat - White, Orange, Green or Red (does not support \"All\")\n PlayermatApi.getInvestigatorName = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n local searchPos = mat.positionToWorld(localInvestigatorPosition)\n local searchResult = searchLib.atPosition(searchPos, \"isCardOrDeck\")\n if #searchResult == 1 then\n return searchResult[1].getName()\n end\n end\n return \"\"\n end\n\n -- Resets the specified skill tracker to \"1, 1, 1, 1\"\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.resetSkillTracker = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"resetSkillTracker\")\n end\n end\n\n -- Redraws the XML for the slot symbols based on the slotData table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.redrawSlotSymbols = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"redrawSlotSymbols\")\n end\n end\n\n -- Finds all objects on the playermat and associated set aside zone and returns a table\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n ---@param filter string Name of the filte function (see util/SearchLib)\n PlayermatApi.searchAroundPlayermat = function(matColor, filter)\n local objList = {}\n for _, mat in pairs(getMatForColor(matColor)) do\n for _, obj in ipairs(mat.call(\"searchAroundSelf\", filter)) do\n table.insert(objList, obj)\n end\n end\n return objList\n end\n\n -- Discard a non-hidden card from the corresponding player's hand\n ---@param matColor string Color of the playermat - White, Orange, Green, Red or All\n PlayermatApi.doDiscardOne = function(matColor)\n for _, mat in pairs(getMatForColor(matColor)) do\n mat.call(\"doDiscardOne\")\n end\n end\n\n -- Triggers the metadata sync for all playermats\n PlayermatApi.syncAllCustomizableCards = function()\n for _, mat in pairs(getMatForColor(\"All\")) do\n mat.call(\"syncAllCustomizableCards\")\n end\n end\n\n return PlayermatApi\nend\nend)\n__bundle_register(\"chaosbag/BlessCurseManagerApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local BlessCurseManagerApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getManager()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"BlessCurseManager\")\n end\n\n -- removes all taken tokens and resets the counts\n BlessCurseManagerApi.removeTakenTokensAndReset = function()\n local BlessCurseManager = getManager()\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Bless\") end, 0.05)\n Wait.time(function() BlessCurseManager.call(\"removeTakenTokens\", \"Curse\") end, 0.10)\n Wait.time(function() BlessCurseManager.call(\"doReset\", \"White\") end, 0.15)\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.sealedToken = function(type, guid)\n getManager().call(\"sealedToken\", { type = type, guid = guid })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n ---@param fromBag? boolean Whether or not token was just drawn from the chaos bag\n BlessCurseManagerApi.releasedToken = function(type, guid, fromBag)\n getManager().call(\"releasedToken\", { type = type, guid = guid, fromBag = fromBag })\n end\n\n -- updates the internal count (called by cards that seal bless/curse tokens)\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n ---@param guid string GUID of the token\n BlessCurseManagerApi.returnedToken = function(type, guid)\n getManager().call(\"returnedToken\", { type = type, guid = guid })\n end\n\n -- broadcasts the current status for bless/curse tokens\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.broadcastStatus = function(playerColor)\n getManager().call(\"broadcastStatus\", playerColor)\n end\n\n -- removes all bless / curse tokens from the chaos bag and play\n ---@param playerColor string Color of the player to show the broadcast to\n BlessCurseManagerApi.removeAll = function(playerColor)\n getManager().call(\"doRemove\", playerColor)\n end\n\n -- adds bless / curse sealing to the hovered card\n ---@param playerColor string Color of the player to show the broadcast to\n ---@param hoveredObject tts__Object Hovered object\n BlessCurseManagerApi.addBlurseSealingMenu = function(playerColor, hoveredObject)\n getManager().call(\"addMenuOptions\", { playerColor = playerColor, hoveredObject = hoveredObject })\n end\n \n -- adds bless / curse to the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.addToken = function(type)\n getManager().call(\"addToken\", type)\n end\n\n -- removes bless / curse from the chaos bag\n ---@param type string Type of chaos token (\"Bless\" or \"Curse\")\n BlessCurseManagerApi.removeToken = function(type)\n getManager().call(\"removeToken\", type)\n end\n \n BlessCurseManagerApi.getBlessCurseInBag = function()\n return getManager().call(\"getBlessCurseInBag\", {})\n end\n\n return BlessCurseManagerApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"core/OptionPanelApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local OptionPanelApi = {}\n\n -- loads saved options\n ---@param options table Set a new state for the option table\n OptionPanelApi.loadSettings = function(options)\n return Global.call(\"loadSettings\", options)\n end\n\n ---@return any: Table of option panel state\n OptionPanelApi.getOptions = function()\n return Global.getTable(\"optionPanel\")\n end\n\n return OptionPanelApi\nend\nend)\n__bundle_register(\"util/SearchLib\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local SearchLib = {}\n local filterFunctions = {\n isCard = function(x) return x.type == \"Card\" end,\n isDeck = function(x) return x.type == \"Deck\" end,\n isCardOrDeck = function(x) return x.type == \"Card\" or x.type == \"Deck\" end,\n isClue = function(x) return x.memo == \"clueDoom\" and x.is_face_down == false end,\n isTileOrToken = function(x) return x.type == \"Tile\" end,\n isUniversalToken = function(x) return x.getMemo() == \"universalActionAbility\" end,\n }\n\n -- performs the actual search and returns a filtered list of object references\n ---@param pos tts__Vector Global position\n ---@param rot? tts__Vector Global rotation\n ---@param size table Size\n ---@param filter? string Name of the filter function\n ---@param direction? table Direction (positive is up)\n ---@param maxDistance? number Distance for the cast\n local function returnSearchResult(pos, rot, size, filter, direction, maxDistance)\n local filterFunc\n if filter then\n filterFunc = filterFunctions[filter]\n end\n local searchResult = Physics.cast({\n origin = pos,\n direction = direction or { 0, 1, 0 },\n orientation = rot or { 0, 0, 0 },\n type = 3,\n size = size,\n max_distance = maxDistance or 0\n })\n\n -- filter the result for matching objects\n local objList = {}\n for _, v in ipairs(searchResult) do\n if not filter or filterFunc(v.hit_object) then\n table.insert(objList, v.hit_object)\n end\n end\n return objList\n end\n\n -- searches the specified area\n SearchLib.inArea = function(pos, rot, size, filter)\n return returnSearchResult(pos, rot, size, filter)\n end\n\n -- searches the area on an object\n SearchLib.onObject = function(obj, filter)\n local pos = obj.getPosition()\n local size = obj.getBounds().size:setAt(\"y\", 1)\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches the specified position (a single point)\n SearchLib.atPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n return returnSearchResult(pos, _, size, filter)\n end\n\n -- searches below the specified position (downwards until y = 0)\n SearchLib.belowPosition = function(pos, filter)\n local size = { 0.1, 2, 0.1 }\n local direction = { 0, -1, 0 }\n local maxDistance = pos.y\n return returnSearchResult(pos, _, size, filter, direction, maxDistance)\n end\n\n return SearchLib\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Tile", @@ -208862,8 +146312,8 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/TokenArranger\")\nend)\n__bundle_register(\"accessories/TokenArranger\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\n\n-- common parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.label = \"\"\nbuttonParameters.tooltip = \"Increase / Decrease\"\nbuttonParameters.color = { 0, 0, 0, 0 }\nbuttonParameters.width = 325\nbuttonParameters.height = 325\n\nlocal inputParameters = {}\ninputParameters.function_owner = self\ninputParameters.font_size = 100\ninputParameters.width = 250\ninputParameters.height = inputParameters.font_size + 23\ninputParameters.alignment = 3\ninputParameters.validation = 2\ninputParameters.tab = 2\n\nlocal percentageLabel = {}\npercentageLabel.function_owner = self\npercentageLabel.click_function = \"none\"\npercentageLabel.width = 0\npercentageLabel.height = 0\n\n-- variables with save function\nlocal tokenPrecedence = {}\nlocal percentage = false\nlocal includeDrawnTokens = true\n\n-- variables without save function\nlocal updating = false\nlocal TOKEN_NAMES = {\n \"Elder Sign\",\n \"Skull\",\n \"Cultist\",\n \"Tablet\",\n \"Elder Thing\",\n \"Auto-fail\",\n \"Bless\",\n \"Curse\",\n \"Frost\",\n \"\"\n}\n\n-- saving the precedence settings and information on the most recently loaded data\nfunction onSave()\n return JSON.encode({\n tokenPrecedence = tokenPrecedence,\n percentage = percentage,\n includeDrawnTokens = includeDrawnTokens\n })\nend\n\n-- loading data, button creation and initial layouting\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n tokenPrecedence = loadedData.tokenPrecedence\n percentage = loadedData.percentage\n includeDrawnTokens = loadedData.includeDrawnTokens\n else\n loadDefaultValues()\n\n -- grab token metadata from mythos area\n Wait.time(function() onTokenDataChanged(mythosAreaApi.returnTokenData()) end, 0.2)\n end\n\n createButtonsAndInputs()\n \n -- maybe trigger layout() to draw percentage buttons\n local objList = getObjectsWithTag(\"tempToken\")\n if #objList \u003e 0 then\n Wait.time(layout, 0.5)\n end\n \n -- context menu items\n self.addContextMenuItem(\"Load default values\", function()\n loadDefaultValues()\n updateUI()\n layout()\n end)\n\n self.addContextMenuItem(\"Include drawn tokens\", function()\n includeDrawnTokens = not includeDrawnTokens\n local text = includeDrawnTokens and \" \" or \" not \"\n broadcastToAll(\"Token Arranger will\" .. text .. \"include currently drawn chaos tokens.\", \"Orange\")\n layout()\n end)\n\n self.addContextMenuItem(\"Toggle percentages\", function()\n if percentage then\n percentage = false\n else\n percentage = \"basic\"\n broadcastToAll(\"Percentages are unreliable when using tokens that draw other tokens (bless or curse for example).\", \"Yellow\")\n end\n layout()\n end)\n\n self.addContextMenuItem(\"Toggle cumulative\", function()\n if percentage == \"cumulative\" then\n percentage = \"basic\"\n else\n percentage = \"cumulative\"\n end\n broadcastToAll(\"Percentages are unreliable when using tokens that draw other tokens (bless or curse for example).\", \"Yellow\")\n layout()\n end)\nend\n\n-- delete temporary tokens when destroyed\nfunction onDestroy() deleteCopiedTokens() end\n\n-- layout tokens when dropped (after 1.5 seconds)\nfunction onDrop() Wait.time(layout, 1.5) end\n\n-- delete temporary tokens when picked up\nfunction onPickUp() deleteCopiedTokens() end\n\n-- click_function for buttons on chaos tokens\nfunction tokenClick(isRightClick, index)\n local change = tonumber(isRightClick and \"-1\" or \"1\")\n tokenPrecedence[TOKEN_NAMES[index]][1] = tokenPrecedence[TOKEN_NAMES[index]][1] + change\n self.editInput({\n index = index - 1,\n value = tokenPrecedence[TOKEN_NAMES[index]][1]\n })\n layout()\nend\n\n-- input_function for input_boxes\nfunction tokenInput(input, selected, index)\n if selected == false then\n local num = tonumber(input)\n if num ~= nil then\n tokenPrecedence[TOKEN_NAMES[index]][1] = num\n end\n layout()\n end\nend\n\n-- loads the default precedence table\nfunction loadDefaultValues()\n -- 1st value: token modifiers for sorting\n -- 2nd value: order for equivalent tokens (starts at 2 because of \"+1\" token)\n tokenPrecedence = {\n [\"Elder Sign\"] = { 100, 2},\n [\"Skull\"] = { -1, 3},\n [\"Cultist\"] = { -2, 4},\n [\"Tablet\"] = { -3, 5},\n [\"Elder Thing\"] = { -4, 6},\n [\"Auto-fail\"] = { -100, 7},\n [\"Bless\"] = { 101, 8},\n [\"Curse\"] = { -101, 9},\n [\"Frost\"] = { -99, 10},\n [\"\"] = { 0, 11}\n }\nend\n\n-- creates buttons and inputs\nfunction createButtonsAndInputs()\n local offset = 0.725\n local pos = { x = { -1.067, 0.377 }, z = -2.175 }\n\n -- button and inputs index 0-9\n for i = 1, 10 do\n if i \u003c 6 then\n buttonParameters.position = { pos.x[1], 0, pos.z + i * offset }\n inputParameters.position = { pos.x[1] + offset, 0.1, pos.z + i * offset }\n else\n buttonParameters.position = { pos.x[2], 0, pos.z + (i - 5) * offset }\n inputParameters.position = { pos.x[2] + offset, 0.1, pos.z + (i - 5) * offset }\n end\n\n buttonParameters.click_function = \"tokenClick\" .. i\n inputParameters.input_function = \"tokenInput\" .. i\n inputParameters.value = tokenPrecedence[TOKEN_NAMES[i]][1]\n\n -- setting click-/inputfunction\n self.setVar(buttonParameters.click_function, function(_, _, isRightClick) tokenClick(isRightClick, i) end)\n self.setVar(inputParameters.input_function, function(_, _, input, selected) tokenInput(input, selected, i) end)\n\n -- button/input creation\n self.createButton(buttonParameters)\n self.createInput(inputParameters)\n end\n\n -- index 10: \"Update / Hide\" button\n self.createButton({\n function_owner = self,\n label = \"Update / Hide\",\n click_function = \"layout\",\n tooltip = \"Left-Click: Update!\\nRight-Click: Hide Tokens!\",\n position = { 0.725, 0.1, 2.025 },\n color = { 1, 1, 1 },\n width = 675,\n height = 175\n })\nend\n\n-- update input fields\nfunction updateUI()\n for i = 1, 10 do\n self.editInput({\n index = i - 1,\n value = tokenPrecedence[TOKEN_NAMES[i]][1]\n })\n end\nend\n\n-- order function for data sorting\nfunction tokenValueComparator(left, right)\n if left.value ~= right.value then\n return left.value \u003e right.value\n elseif left.order ~= right.order then\n return left.order \u003c right.order\n else\n return false\n end\nend\n\n-- deletes previously placed tokens\nfunction deleteCopiedTokens()\n for _, token in ipairs(getObjectsWithTag(\"tempToken\")) do\n token.destruct()\n end\n\n -- this removes the percentage buttons (by index 11+)\n local buttonCount = #self.getButtons()\n if buttonCount \u003c 12 then return end\n\n for i = buttonCount, 12, -1 do\n self.removeButton(i - 1)\n end\nend\n\n-- creates buttons as labels as display for percentage values\nfunction createPercentageButton(tokenCount, valueCount, tokenName)\n local startPos = Vector(2.3, -0.04, 0.875 * valueCount)\n\n if percentage == \"cumulative\" then\n percentageLabel.scale = { 1.5, 1.5, 1.5 }\n percentageLabel.position = startPos - Vector(0, 0, 2.85)\n else\n percentageLabel.scale = { 2, 2, 2 }\n percentageLabel.position = startPos - Vector(0, 0, 2.675)\n end\n\n -- determine font_color\n if tokenName == \"Elder Sign\" then\n percentageLabel.font_color = { 0.35, 0.71, 0.85 }\n elseif tokenName == \"Auto-fail\" then\n percentageLabel.font_color = { 0.86, 0.1, 0.1 }\n -- check if the tokenName contains letters (e.g. symbol token)\n elseif string.match(tokenName, \"%a\") ~= nil then\n percentageLabel.font_color = { 0.68, 0.53, 0.86 }\n else\n percentageLabel.font_color = { 0.85, 0.67, 0.33 }\n end\n\n -- create label for base percentage\n local basePercentage = math.floor((tokenCount.row / tokenCount.total) * 10000) / 100\n percentageLabel.label = string.format(\"%s\", string.format(\"%05.2f\", basePercentage) .. \"%\")\n self.createButton(percentageLabel)\n\n -- optionally create label for cumulative percentage\n if percentage == \"cumulative\" then\n percentageLabel.position = startPos - Vector(0, 0, 2.45)\n percentageLabel.font_color = { 1, 1, 1 }\n\n -- only display one digit for 100%\n if tokenCount.sum == tokenCount.total then\n percentageLabel.label = \"100.0%\"\n else\n local cumulativePercentage = math.floor((tokenCount.sum / tokenCount.total) * 10000) / 100\n percentageLabel.label = string.format(\"%s\", string.format(\"%05.2f\", cumulativePercentage) .. \"%\")\n end\n self.createButton(percentageLabel)\n end\nend\n\n-- main function (delete old tokens, clone chaos bag content, sort it and position it)\nfunction layout(_, _, isRightClick)\n if updating then return end\n updating = true\n deleteCopiedTokens()\n\n -- stop here if right-clicked\n if isRightClick then\n updating = false\n return\n end\n\n -- get ChaosBag and stop if not found\n local chaosBag = chaosBagApi.findChaosBag()\n if not chaosBag then\n updating = false\n return\n end\n\n -- clone tokens from chaos bag (default position above trash can)\n local rawData = chaosBag.getData().ContainedObjects\n\n -- optionally get the data for tokens in play\n if includeDrawnTokens then\n for _, token in pairs(chaosBagApi.getTokensInPlay()) do\n if token ~= nil then table.insert(rawData, token.getData()) end\n end\n end\n\n -- generate layout data\n local data = {}\n for i, objData in ipairs(rawData) do\n objData[\"Tags\"] = { \"tempToken\" }\n local value = tonumber(objData.Nickname)\n local precedence = tokenPrecedence[objData.Nickname]\n\n -- remove GUID to avoid issues for high latency clients\n objData[\"GUID\"] = nil\n\n -- store data with value / precendence\n data[i] = {\n token = objData,\n value = value or precedence[1]\n }\n\n -- order for comparator function\n if precedence ~= nil then\n data[i].order = precedence[2]\n else\n data[i].order = value\n end\n end\n\n -- sort table by value (symbols last if same value)\n table.sort(data, tokenValueComparator)\n\n -- laying out the tokens\n local pos = self.getPosition() + Vector(3.55, -0.05, -3.95)\n if percentage then pos.z = pos.z - 3.05 end\n\n local location = { x = pos.x, y = pos.y, z = pos.z }\n local rotation = self.getRotation()\n local currentValue = data[1].value\n local tokenCount = { row = 0, sum = 0, total = #data }\n local valueCount = 1\n local tokenName = false\n\n for i, item in ipairs(data) do\n -- this is true for the first token in a new row\n if item.value ~= currentValue then\n if percentage then\n tokenCount.sum = tokenCount.sum + tokenCount.row\n createPercentageButton(tokenCount, valueCount, tokenName)\n end\n\n location.x = location.x - 1.75\n location.z = pos.z\n currentValue = item.value\n valueCount = valueCount + 1\n tokenCount.row = 0\n end\n\n spawnObjectData({\n data = item.token,\n position = location,\n rotation = rotation\n })\n tokenName = item.token.Nickname\n location.z = location.z - 1.75\n tokenCount.row = tokenCount.row + 1\n end\n\n -- this is repeated to create the button for the last token\n if percentage then\n tokenCount.sum = tokenCount.sum + tokenCount.row\n createPercentageButton(tokenCount, valueCount, tokenName)\n end\n\n -- introducing a small delay to limit update calls\n Wait.time(function() updating = false end, 0.1)\nend\n\n-- called from outside to set default values for tokens\nfunction onTokenDataChanged(parameters)\n local tokenData = parameters.tokenData or {}\n local currentScenario = parameters.currentScenario or \"\"\n local useFrontData = parameters.useFrontData\n\n -- update token precedence\n for key, table in pairs(tokenData) do\n local modifier = table.modifier\n if modifier == -999 then modifier = 0 end\n tokenPrecedence[key][1] = modifier\n end\n\n updateUI()\n layout()\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n \n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right \n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n \n return MythosAreaApi\nend\nend)\nreturn __bundle_require(\"__root\")", - "LuaScriptState": "{\"includeDrawnTokens\":true,\"percentage\":false,\"tokenPrecedence\":{\"\":[0,11],\"Auto-fail\":[-100,7],\"Bless\":[101,8],\"Cultist\":[-2,4],\"Curse\":[-101,9],\"Elder Sign\":[100,2],\"Elder Thing\":[-4,6],\"Frost\":[-99,10],\"Skull\":[-1,3],\"Tablet\":[-3,5]}}", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"accessories/TokenArranger\")\nend)\n__bundle_register(\"accessories/TokenArranger\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\nlocal mythosAreaApi = require(\"core/MythosAreaApi\")\n\n-- common parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.label = \"\"\nbuttonParameters.tooltip = \"Increase / Decrease\"\nbuttonParameters.color = { 0, 0, 0, 0 }\nbuttonParameters.width = 325\nbuttonParameters.height = 325\n\nlocal inputParameters = {}\ninputParameters.function_owner = self\ninputParameters.font_size = 200\ninputParameters.width = 500\ninputParameters.height = inputParameters.font_size + 46\ninputParameters.alignment = 3\ninputParameters.validation = 2\ninputParameters.tab = 2\ninputParameters.scale = { 0.5, 0.5, 0.5 }\n\nlocal percentageLabel = {}\npercentageLabel.function_owner = self\npercentageLabel.click_function = \"none\"\npercentageLabel.font_size = 200\npercentageLabel.width = 0\npercentageLabel.height = 0\n\n-- variables with save function\nlocal tokenPrecedence = {}\nlocal percentage = false\nlocal includeDrawnTokens = true\n\n-- variables without save function\nlocal updating = false\nlocal TOKEN_NAMES = {\n \"Elder Sign\",\n \"Skull\",\n \"Cultist\",\n \"Tablet\",\n \"Elder Thing\",\n \"Auto-fail\",\n \"Bless\",\n \"Curse\",\n \"Frost\",\n \"\"\n}\n\n-- saving the precedence settings and information on the most recently loaded data\nfunction onSave()\n return JSON.encode({\n tokenPrecedence = tokenPrecedence,\n percentage = percentage,\n includeDrawnTokens = includeDrawnTokens\n })\nend\n\n-- loading data, button creation and initial layouting\nfunction onLoad(savedData)\n if savedData and savedData ~= \"\" then\n local loadedData = JSON.decode(savedData)\n tokenPrecedence = loadedData.tokenPrecedence\n percentage = loadedData.percentage\n includeDrawnTokens = loadedData.includeDrawnTokens\n else\n loadDefaultValues()\n\n -- grab token metadata from mythos area\n Wait.time(function() onTokenDataChanged(mythosAreaApi.returnTokenData()) end, 0.2)\n end\n\n createButtonsAndInputs()\n\n -- maybe trigger layout() to draw percentage buttons\n local objList = getObjectsWithTag(\"tempToken\")\n if #objList \u003e 0 then\n Wait.time(layout, 0.5)\n end\n\n -- context menu items\n self.addContextMenuItem(\"Load default values\", function()\n loadDefaultValues()\n updateUI()\n layout()\n end)\n\n self.addContextMenuItem(\"Include drawn tokens\", function()\n includeDrawnTokens = not includeDrawnTokens\n local text = includeDrawnTokens and \" \" or \" not \"\n broadcastToAll(\"Token Arranger will\" .. text .. \"include currently drawn chaos tokens.\", \"Orange\")\n layout()\n end)\n\n self.addContextMenuItem(\"Toggle percentages\", function()\n if percentage then\n percentage = false\n else\n percentage = \"basic\"\n broadcastToAll(\"Percentages are unreliable when using tokens that draw other tokens (bless or curse for example).\", \"Yellow\")\n end\n layout()\n end)\n\n self.addContextMenuItem(\"Toggle cumulative\", function()\n if percentage == \"cumulative\" then\n percentage = \"basic\"\n else\n percentage = \"cumulative\"\n end\n broadcastToAll(\"Percentages are unreliable when using tokens that draw other tokens (bless or curse for example).\", \"Yellow\")\n layout()\n end)\nend\n\n-- delete temporary tokens when destroyed\nfunction onDestroy() deleteCopiedTokens() end\n\n-- layout tokens when dropped (after 1.5 seconds)\nfunction onDrop() Wait.time(layout, 1.5) end\n\n-- delete temporary tokens when picked up\nfunction onPickUp() deleteCopiedTokens() end\n\n-- click_function for buttons on chaos tokens\nfunction tokenClick(isRightClick, index)\n local change = tonumber(isRightClick and \"-1\" or \"1\")\n tokenPrecedence[TOKEN_NAMES[index]][1] = tokenPrecedence[TOKEN_NAMES[index]][1] + change\n self.editInput({\n index = index - 1,\n value = tokenPrecedence[TOKEN_NAMES[index]][1]\n })\n layout()\nend\n\n-- input_function for input_boxes\nfunction tokenInput(input, selected, index)\n if selected == false then\n local num = tonumber(input)\n if num ~= nil then\n tokenPrecedence[TOKEN_NAMES[index]][1] = num\n end\n layout()\n end\nend\n\n-- loads the default precedence table\nfunction loadDefaultValues()\n -- 1st value: token modifiers for sorting\n -- 2nd value: order for equivalent tokens (starts at 2 because of \"+1\" token)\n tokenPrecedence = {\n [\"Elder Sign\"] = { 100, 2},\n [\"Skull\"] = { -1, 3},\n [\"Cultist\"] = { -2, 4},\n [\"Tablet\"] = { -3, 5},\n [\"Elder Thing\"] = { -4, 6},\n [\"Auto-fail\"] = { -100, 7},\n [\"Bless\"] = { 110, 8},\n [\"Curse\"] = { -110, 9},\n [\"Frost\"] = { -105, 10},\n [\"\"] = { 0, 11}\n }\nend\n\n-- creates buttons and inputs\nfunction createButtonsAndInputs()\n local offset = 0.725\n local pos = { x = { -1.067, 0.377 }, z = -2.175 }\n\n -- button and inputs index 0-9\n for i = 1, 10 do\n if i \u003c 6 then\n buttonParameters.position = { pos.x[1], 0, pos.z + i * offset }\n inputParameters.position = { pos.x[1] + offset, 0.1, pos.z + i * offset }\n else\n buttonParameters.position = { pos.x[2], 0, pos.z + (i - 5) * offset }\n inputParameters.position = { pos.x[2] + offset, 0.1, pos.z + (i - 5) * offset }\n end\n\n buttonParameters.click_function = \"tokenClick\" .. i\n inputParameters.input_function = \"tokenInput\" .. i\n inputParameters.value = tokenPrecedence[TOKEN_NAMES[i]][1]\n\n -- setting click-/inputfunction\n self.setVar(buttonParameters.click_function, function(_, _, isRightClick) tokenClick(isRightClick, i) end)\n self.setVar(inputParameters.input_function, function(_, _, input, selected) tokenInput(input, selected, i) end)\n\n -- button/input creation\n self.createButton(buttonParameters)\n self.createInput(inputParameters)\n end\n\n -- index 10: \"Update / Hide\" button\n self.createButton({\n function_owner = self,\n label = \"Update / Hide\",\n click_function = \"layout\",\n tooltip = \"Left-Click: Update!\\nRight-Click: Hide Tokens!\",\n position = { 0.725, 0.1, 2.025 },\n scale = { 0.5, 0.5, 0.5 },\n color = { 1, 1, 1 },\n font_size = 200,\n width = 1350,\n height = 325\n })\nend\n\n-- update input fields\nfunction updateUI()\n for i = 1, 10 do\n self.editInput({\n index = i - 1,\n value = tokenPrecedence[TOKEN_NAMES[i]][1]\n })\n end\nend\n\n-- order function for data sorting\nfunction tokenValueComparator(left, right)\n if left.value ~= right.value then\n return left.value \u003e right.value\n elseif left.order ~= right.order then\n return left.order \u003c right.order\n else\n return false\n end\nend\n\n-- deletes previously placed tokens\nfunction deleteCopiedTokens()\n for _, token in ipairs(getObjectsWithTag(\"tempToken\")) do\n token.destruct()\n end\n\n -- this removes the percentage buttons (by index 11+)\n local buttonCount = #self.getButtons()\n if buttonCount \u003c 12 then return end\n\n for i = buttonCount, 12, -1 do\n self.removeButton(i - 1)\n end\nend\n\n-- creates buttons as labels as display for percentage values\nfunction createPercentageButton(tokenCount, rowCount, tokenName)\n local startPos = Vector(2.3, -0.04, 0.875 * rowCount)\n\n if percentage == \"cumulative\" then\n percentageLabel.scale = { 0.75, 0.75, 0.75 }\n percentageLabel.position = startPos - Vector(0, 0, 2.85)\n else\n percentageLabel.scale = { 1, 1, 1 }\n percentageLabel.position = startPos - Vector(0, 0, 2.675)\n end\n\n -- determine font_color\n if tokenName == \"Elder Sign\" then\n percentageLabel.font_color = { 0.35, 0.71, 0.85 }\n elseif tokenName == \"Auto-fail\" then\n percentageLabel.font_color = { 0.86, 0.1, 0.1 }\n elseif string.match(tokenName, \"%a\") ~= nil then\n -- tokenName contains letters (e.g. symbol token)\n percentageLabel.font_color = { 0.68, 0.53, 0.86 }\n else\n percentageLabel.font_color = { 0.85, 0.67, 0.33 }\n end\n\n -- create label for base percentage\n local basePercentage = math.floor((tokenCount.row / tokenCount.total) * 10000) / 100\n percentageLabel.label = string.format(\"%s\", string.format(\"%05.2f\", basePercentage) .. \"%\")\n self.createButton(percentageLabel)\n\n -- optionally create label for cumulative percentage\n if percentage == \"cumulative\" then\n percentageLabel.position = startPos - Vector(0, 0, 2.45)\n percentageLabel.font_color = { 1, 1, 1 }\n\n -- only display one digit for 100%\n if tokenCount.sum == tokenCount.total then\n percentageLabel.label = \"100.0%\"\n else\n local cumulativePercentage = math.floor((tokenCount.sum / tokenCount.total) * 10000) / 100\n percentageLabel.label = string.format(\"%s\", string.format(\"%05.2f\", cumulativePercentage) .. \"%\")\n end\n self.createButton(percentageLabel)\n end\nend\n\n-- main function (delete old tokens, clone chaos bag content, sort it and position it)\nfunction layout(_, _, isRightClick)\n if updating then return end\n updating = true\n deleteCopiedTokens()\n\n -- stop here if right-clicked\n if isRightClick then\n updating = false\n return\n end\n\n -- get ChaosBag and stop if not found\n local chaosBag = chaosBagApi.findChaosBag()\n if not chaosBag then\n updating = false\n return\n end\n\n -- clone tokens from chaos bag (default position above trash can)\n local rawData = chaosBag.getData().ContainedObjects or {}\n\n -- optionally get the data for tokens in play\n if includeDrawnTokens then\n for _, token in pairs(chaosBagApi.getTokensInPlay()) do\n if token ~= nil then\n table.insert(rawData, token.getData())\n end\n end\n end\n\n -- generate layout data\n local data = {}\n for i, objData in ipairs(rawData) do\n objData[\"Tags\"] = { \"tempToken\" }\n local value = tonumber(objData.Nickname)\n local precedence = tokenPrecedence[objData.Nickname]\n\n -- remove GUID to avoid issues for high latency clients\n objData[\"GUID\"] = nil\n\n -- store data with value / precendence\n data[i] = {\n token = objData,\n value = value or precedence[1]\n }\n\n -- order for comparator function\n if precedence ~= nil then\n data[i].order = precedence[2]\n else\n data[i].order = value\n end\n end\n\n -- handling for near empty chaos bag\n if #data == 0 then\n -- small delay to limit update calls\n Wait.time(function() updating = false end, 0.1)\n return\n elseif #data \u003e 1 then\n -- sort table by value (symbols last if same value)\n table.sort(data, tokenValueComparator)\n end\n\n -- laying out the tokens\n local pos = self.getPosition() + Vector(3.55, -0.05, -3.95)\n if percentage then pos.z = pos.z - 3.05 end\n\n local location = { x = pos.x, y = pos.y, z = pos.z }\n local rotation = self.getRotation()\n local currentValue = data[1].value\n local tokenCount = { row = 0, sum = 0, total = #data }\n local rowCount = 1\n local tokenName\n\n for _, item in ipairs(data) do\n -- this is true for the first token in a new row\n if item.value ~= currentValue then\n if percentage then\n tokenCount.sum = tokenCount.sum + tokenCount.row\n createPercentageButton(tokenCount, rowCount, tokenName)\n end\n\n location.x = location.x - 1.75\n location.z = pos.z\n currentValue = item.value\n rowCount = rowCount + 1\n tokenCount.row = 0\n end\n\n spawnObjectData({\n data = item.token,\n position = location,\n rotation = rotation\n })\n tokenName = item.token.Nickname\n location.z = location.z - 1.75\n tokenCount.row = tokenCount.row + 1\n end\n\n -- this is repeated to create the button for the last token\n if percentage then\n tokenCount.sum = tokenCount.sum + tokenCount.row\n createPercentageButton(tokenCount, rowCount, tokenName)\n end\n\n -- small delay to limit update calls\n Wait.time(function() updating = false end, 0.1)\nend\n\n-- called from outside to set default values for tokens\nfunction onTokenDataChanged(parameters)\n -- update token precedence\n for key, table in pairs(parameters.tokenData or {}) do\n local modifier = table.modifier\n if modifier == -999 then modifier = 0 end\n tokenPrecedence[key][1] = modifier\n end\n\n updateUI()\n layout()\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\n__bundle_register(\"core/MythosAreaApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local MythosAreaApi = {}\n local guidReferenceApi = require(\"core/GUIDReferenceApi\")\n\n local function getMythosArea()\n return guidReferenceApi.getObjectByOwnerAndType(\"Mythos\", \"MythosArea\")\n end\n\n ---@return any: Table of chaos token metadata (if provided through scenario reference card)\n MythosAreaApi.returnTokenData = function()\n return getMythosArea().call(\"returnTokenData\")\n end\n\n ---@return any: Object reference to the encounter deck\n MythosAreaApi.getEncounterDeck = function()\n return getMythosArea().call(\"getEncounterDeck\")\n end\n\n -- draw an encounter card for the requesting mat to the first empty spot from the right\n ---@param matColor string Playermat that triggered this\n ---@param position tts__Vector Position for the encounter card\n MythosAreaApi.drawEncounterCard = function(matColor, position)\n getMythosArea().call(\"drawEncounterCard\", { matColor = matColor, position = position })\n end\n\n -- reshuffle the encounter deck\n MythosAreaApi.reshuffleEncounterDeck = function()\n getMythosArea().call(\"reshuffleEncounterDeck\")\n end\n\n return MythosAreaApi\nend\nend)\n__bundle_register(\"core/GUIDReferenceApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local GUIDReferenceApi = {}\n\n local function getGuidHandler()\n return getObjectFromGUID(\"123456\")\n end\n\n -- Returns the matching object\n ---@param owner string Parent object for this search\n ---@param type string Type of object to search for\n ---@return any: Object reference to the matching object\n GUIDReferenceApi.getObjectByOwnerAndType = function(owner, type)\n return getGuidHandler().call(\"getObjectByOwnerAndType\", { owner = owner, type = type })\n end\n\n -- Returns all matching objects as a table with references\n ---@param type string Type of object to search for\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByType = function(type)\n return getGuidHandler().call(\"getObjectsByType\", type)\n end\n\n -- Returns all matching objects as a table with references\n ---@param owner string Parent object for this search\n ---@return table: List of object references to matching objects\n GUIDReferenceApi.getObjectsByOwner = function(owner)\n return getGuidHandler().call(\"getObjectsByOwner\", owner)\n end\n\n -- Sends new information to the reference handler to edit the main index\n ---@param owner string Parent of the object\n ---@param type string Type of the object\n ---@param guid string GUID of the object\n GUIDReferenceApi.editIndex = function(owner, type, guid)\n return getGuidHandler().call(\"editIndex\", {\n owner = owner,\n type = type,\n guid = guid\n })\n end\n\n -- Returns the owner of an object or the object it's located on\n ---@param object tts__GameObject Object for this search\n ---@return string: Parent of the object or object it's located on\n GUIDReferenceApi.getOwnerOfObject = function(object)\n return getGuidHandler().call(\"getOwnerOfObject\", object)\n end\n\n return GUIDReferenceApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"includeDrawnTokens\":true,\"percentage\":false,\"tokenPrecedence\":{\"\":[0,11],\"Auto-fail\":[-100,7],\"Bless\":[110,8],\"Cultist\":[-2,4],\"Curse\":[-110,9],\"Elder Sign\":[100,2],\"Elder Thing\":[-4,6],\"Frost\":[-105,10],\"Skull\":[-1,3],\"Tablet\":[-3,5]}}", "MeasureMovement": false, "Name": "Custom_Token", "Nickname": "Token Arranger", @@ -208919,7 +146369,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"chaosbag/ChaosBagManager\")\nend)\n__bundle_register(\"chaosbag/ChaosBagManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\n\nlocal TOKEN_IDS = {\n -- first row\n \"p1\", \"0\", \"m1\", \"m2\", \"m3\", \"m4\",\n -- second row\n \"m5\", \"m6\", \"m7\", \"m8\", \"frost\",\n -- third row\n \"blue\", \"skull\", \"cultist\", \"tablet\", \"elder\", \"red\"\n}\n\nlocal BUTTON_TOOLTIP = {\n -- first row\n \"+1\", \"0\", \"-1\", \"-2\", \"-3\", \"-4\",\n -- second row\n \"-5\", \"-6\", \"-7\", \"-8\", \"Frost\",\n -- third row\n \"Elder Sign\", \"Skull\", \"Cultist\", \"Tablet\", \"Elder Thing\", \"Auto-fail\"\n}\n\nlocal BUTTON_POSITION_X = {\n -- first row\n -0.9, -0.54, -0.18, 0.18, 0.54, 0.9,\n -- second row\n -0.9, -0.54, -0.18, 0.18, 0.9,\n -- third row\n -0.9, -0.54, -0.18, 0.18, 0.54, 0.9\n}\n\nlocal BUTTON_POSITION_Z = { -0.298, 0.05, 0.399 }\n\n-- common button parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.color = { 0, 0, 0, 0 }\nbuttonParameters.width = 160\nbuttonParameters.height = 160\n\nfunction onLoad()\n -- create buttons for tokens\n for i = 1, #BUTTON_POSITION_X do\n local funcName = \"buttonClick\" .. i\n self.setVar(funcName, function(_, _, isRightClick) buttonClick(i, isRightClick) end)\n\n buttonParameters.click_function = funcName\n buttonParameters.tooltip = BUTTON_TOOLTIP[i]\n buttonParameters.position = { x = BUTTON_POSITION_X[i], y = 0 }\n\n if i \u003c 7 then\n buttonParameters.position.z = BUTTON_POSITION_Z[1]\n elseif i \u003c 12 then\n buttonParameters.position.z = BUTTON_POSITION_Z[2]\n else\n buttonParameters.position.z = BUTTON_POSITION_Z[3]\n end\n\n self.createButton(buttonParameters)\n end\nend\n\n-- click function for buttons\nfunction buttonClick(index, isRightClick)\n local tokenId = TOKEN_IDS[index]\n\n if isRightClick then\n chaosBagApi.removeChaosToken(tokenId)\n else\n local tokens = {}\n local name = BUTTON_TOOLTIP[index]\n local chaosbag = chaosBagApi.findChaosBag()\n\n for _, v in ipairs(chaosbag.getObjects()) do\n if v.name == name then table.insert(tokens, v.guid) end\n end\n\n -- spawn token (only 8 frost tokens allowed)\n if tokenId == \"frost\" and #tokens == 8 then\n printToAll(\"The maximum of 8 Frost tokens is already in the bag.\", \"Yellow\")\n return\n end\n\n chaosBagApi.spawnChaosToken(tokenId)\n printToAll(\"Adding \" .. name .. \" token (in bag: \" .. #tokens + 1 .. \")\", \"White\")\n end\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n return Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n return Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n return Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n return Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ChaosBagApi.returnChaosTokenToBag = function(token)\n return Global.call(\"returnChaosTokenToBag\", token)\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n return Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any canTouch True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- called by playermats (by the \"Draw chaos token\" button)\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param returnedToken? tts__Object Token to be replaced with newly drawn token\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, returnedToken)\n return Global.call(\"drawChaosToken\", {mat = mat, drawAdditional = drawAdditional, tokenType = tokenType, guidToBeResolved = guidToBeResolved, returnedToken = returnedToken})\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"chaosbag/ChaosBagManager\")\nend)\n__bundle_register(\"chaosbag/ChaosBagManager\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal chaosBagApi = require(\"chaosbag/ChaosBagApi\")\n\nlocal TOKEN_IDS = {\n -- first row\n \"p1\", \"0\", \"m1\", \"m2\", \"m3\", \"m4\",\n -- second row\n \"m5\", \"m6\", \"m7\", \"m8\", \"frost\",\n -- third row\n \"blue\", \"skull\", \"cultist\", \"tablet\", \"elder\", \"red\"\n}\n\nlocal BUTTON_TOOLTIP = {\n -- first row\n \"+1\", \"0\", \"-1\", \"-2\", \"-3\", \"-4\",\n -- second row\n \"-5\", \"-6\", \"-7\", \"-8\", \"Frost\",\n -- third row\n \"Elder Sign\", \"Skull\", \"Cultist\", \"Tablet\", \"Elder Thing\", \"Auto-fail\"\n}\n\nlocal BUTTON_POSITION_X = {\n -- first row\n -0.9, -0.54, -0.18, 0.18, 0.54, 0.9,\n -- second row\n -0.9, -0.54, -0.18, 0.18, 0.9,\n -- third row\n -0.9, -0.54, -0.18, 0.18, 0.54, 0.9\n}\n\nlocal BUTTON_POSITION_Z = { -0.298, 0.05, 0.399 }\n\n-- common button parameters\nlocal buttonParameters = {}\nbuttonParameters.function_owner = self\nbuttonParameters.color = { 0, 0, 0, 0 }\nbuttonParameters.width = 160\nbuttonParameters.height = 160\n\nfunction onLoad()\n -- create buttons for tokens\n for i = 1, #BUTTON_POSITION_X do\n local funcName = \"buttonClick\" .. i\n self.setVar(funcName, function(_, _, isRightClick) buttonClick(i, isRightClick) end)\n\n buttonParameters.click_function = funcName\n buttonParameters.tooltip = BUTTON_TOOLTIP[i]\n buttonParameters.position = { x = BUTTON_POSITION_X[i], y = 0 }\n\n if i \u003c 7 then\n buttonParameters.position.z = BUTTON_POSITION_Z[1]\n elseif i \u003c 12 then\n buttonParameters.position.z = BUTTON_POSITION_Z[2]\n else\n buttonParameters.position.z = BUTTON_POSITION_Z[3]\n end\n\n self.createButton(buttonParameters)\n end\nend\n\n-- click function for buttons\nfunction buttonClick(index, isRightClick)\n local tokenId = TOKEN_IDS[index]\n\n if isRightClick then\n chaosBagApi.removeChaosToken(tokenId)\n else\n local tokens = {}\n local name = BUTTON_TOOLTIP[index]\n local chaosbag = chaosBagApi.findChaosBag()\n\n for _, v in ipairs(chaosbag.getObjects()) do\n if v.name == name then table.insert(tokens, v.guid) end\n end\n\n -- spawn token (only 8 frost tokens allowed)\n if tokenId == \"frost\" and #tokens == 8 then\n printToAll(\"The maximum of 8 Frost tokens is already in the bag.\", \"Yellow\")\n return\n end\n\n chaosBagApi.spawnChaosToken(tokenId)\n printToAll(\"Adding \" .. name .. \" token (in bag: \" .. #tokens + 1 .. \")\", \"White\")\n end\nend\nend)\n__bundle_register(\"chaosbag/ChaosBagApi\", function(require, _LOADED, __bundle_register, __bundle_modules)\ndo\n local ChaosBagApi = {}\n\n -- respawns the chaos bag with a new state of tokens\n ---@param tokenList table List of chaos token ids\n ChaosBagApi.setChaosBagState = function(tokenList)\n Global.call(\"setChaosBagState\", tokenList)\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getChaosBagState = function()\n local chaosBagContentsCatcher = Global.call(\"getChaosBagState\")\n local chaosBagContents = {}\n for _, v in ipairs(chaosBagContentsCatcher) do\n table.insert(chaosBagContents, v)\n end\n return chaosBagContents\n end\n\n -- checks scripting zone for chaos bag (also called by a lot of objects!)\n ChaosBagApi.findChaosBag = function()\n return Global.call(\"findChaosBag\")\n end\n\n -- returns a table of object references to the tokens in play (does not include sealed tokens!)\n ChaosBagApi.getTokensInPlay = function()\n return Global.call(\"getChaosTokensinPlay\")\n end\n\n -- returns all sealed tokens on cards to the chaos bag\n ---@param playerColor string Color of the player to show the broadcast to\n ChaosBagApi.releaseAllSealedTokens = function(playerColor)\n Global.call(\"releaseAllSealedTokens\", playerColor)\n end\n\n -- returns all drawn tokens to the chaos bag\n ChaosBagApi.returnChaosTokens = function()\n Global.call(\"returnChaosTokens\")\n end\n\n -- removes the specified chaos token from the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.removeChaosToken = function(id)\n Global.call(\"removeChaosToken\", id)\n end\n\n -- returns a chaos token to the bag and calls all relevant functions\n ---@param token tts__Object Chaos token to return\n ---@param fromBag boolean whether or not the token to return was in the middle of being drawn (true) or elsewhere (false)\n ChaosBagApi.returnChaosTokenToBag = function(token, fromBag)\n Global.call(\"returnChaosTokenToBag\", { token = token, fromBag = fromBag })\n end\n\n -- spawns the specified chaos token and puts it into the chaos bag\n ---@param id string ID of the chaos token\n ChaosBagApi.spawnChaosToken = function(id)\n Global.call(\"spawnChaosToken\", id)\n end\n\n -- Checks to see if the chaos bag can be manipulated. If a player is searching the bag when tokens\n -- are drawn or replaced a TTS bug can cause those tokens to vanish. Any functions which change the\n -- contents of the bag should check this method before doing so.\n -- This method will broadcast a message to all players if the bag is being searched.\n ---@return any: True if the bag is manipulated, false if it should be blocked.\n ChaosBagApi.canTouchChaosTokens = function()\n return Global.call(\"canTouchChaosTokens\")\n end\n\n -- draws a chaos token to a playermat\n ---@param mat tts__Object Playermat that triggered this\n ---@param drawAdditional boolean Controls whether additional tokens should be drawn\n ---@param tokenType? string Name of token (e.g. \"Bless\") to be drawn from the bag\n ---@param guidToBeResolved? string GUID of the sealed token to be resolved instead of drawing a token from the bag\n ---@param takeParameters? table Position and rotation of the location where the new token should be drawn to, usually to replace a returned token\n ---@return tts__Object: Object reference to the token that was drawn\n ChaosBagApi.drawChaosToken = function(mat, drawAdditional, tokenType, guidToBeResolved, takeParameters)\n return Global.call(\"drawChaosToken\", {\n mat = mat,\n drawAdditional = drawAdditional,\n tokenType = tokenType,\n guidToBeResolved = guidToBeResolved,\n takeParameters = takeParameters\n })\n end\n\n -- returns a Table List of chaos token ids in the current chaos bag\n -- requires copying the data into a new table because TTS is weird about handling table return values in Global\n ChaosBagApi.getIdUrlMap = function()\n return Global.getTable(\"ID_URL_MAP\")\n end\n\n return ChaosBagApi\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Tile", @@ -208967,7 +146417,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": true, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "BlockRectangle", @@ -209006,16 +146456,16 @@ "CastShadows": true, "ColliderURL": "", "Convex": true, - "DiffuseURL": "https://i.ibb.co/1GLSncs/title.jpg", + "DiffuseURL": "http://cloud-3.steamusercontent.com/ugc/2466364192777858421/669B09277F042A9A1328F1D62D79D9221D4B4D53/", "MaterialIndex": 3, "MeshURL": "https://raw.githubusercontent.com/RobMayer/TTSLibrary/master/advboxes/core_h_MSH.obj", "NormalURL": "", "TypeIndex": 0 }, - "Description": "by Frying Tonight", + "Description": "by RudeRugg", "DragSelectable": true, - "GMNotes": "fancreations/campaign_the_matter_of_britain.json", - "GUID": "194cc5", + "GMNotes": "fancreations/campaign_lovecrafter_3077.json", + "GUID": "b08d20", "Grid": true, "GridProjection": false, "Hands": false, @@ -209023,15 +146473,14 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n local notes = self.getGMNotes()\n\n -- default parameters (e.g. scenarios)\n local buttonParameters = {\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = { x = 0, y = 0.1, z = 2.1 },\n height = 250,\n width = 800,\n font_size = 150,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 }\n }\n\n -- return to boxes\n if string.match(notes, \"................\") == \"campaigns/return\" then\n buttonParameters.position.z = 2\n\n -- official campaign boxes\n elseif string.match(notes, \".........\") == \"campaigns\" or self.hasTag(\"LargeBox\") then\n buttonParameters.position.z = 6\n buttonParameters.height = 500\n buttonParameters.width = 1700\n buttonParameters.font_size = 350\n\n -- investigator boxes\n elseif string.match(notes, \".............\") == \"investigators\" then\n buttonParameters.position.z = 7\n buttonParameters.height = 850\n buttonParameters.width = 3400\n buttonParameters.font_size = 700\n end\n\n self.createButton(buttonParameters)\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', { url = self.getGMNotes(), player = playerColor and Player[playerColor] or nil, replace = self.guid })\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/DownloadBox\")\nend)\n__bundle_register(\"core/DownloadBox\", function(require, _LOADED, __bundle_register, __bundle_modules)\nfunction onLoad()\n -- make sure the model is loaded so that we can use the bounds\n Wait.condition(buttonCreation, function() return not self.loading_custom end)\nend\n\n-- dynamic download button position based on model\nfunction buttonCreation()\n local scale = self.getScale()\n local bounds = self.getBoundsNormalized()\n\n self.createButton({\n label = \"Download\",\n click_function = \"buttonClick_download\",\n function_owner = self,\n position = {\n x = 0,\n y = -(bounds.size.y / 2 + bounds.offset.y) / scale.y + 0.5,\n z = (bounds.size.z / 2 + 1.2) / scale.z\n },\n height = 700,\n width = 2300,\n font_size = 430,\n color = { 0, 0, 0 },\n font_color = { 1, 1, 1 },\n scale = { 1 / scale.x, 1, 1 / scale.z }\n })\nend\n\nfunction buttonClick_download(_, playerColor)\n Global.call('placeholder_download', {\n url = self.getGMNotes(),\n player = playerColor and Player[playerColor] or nil,\n replace = self.guid\n })\nend\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "Custom_Model", - "Nickname": " The Matter of Britain", + "Nickname": " Lovecrafter 3077", "Snap": true, "Sticky": true, "Tags": [ - "CampaignBox", "LargeBox" ], "Tooltip": true, @@ -210155,7 +147604,7 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PhysicsDetector\")\nend)\n__bundle_register(\"core/PhysicsDetector\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- will notify the user to enable physics if it appears to not be fully enabled\n\n-- this event should only fire if physics aren't fully enabled\nfunction onCollisionExit()\n broadcastToAll(\"Physics disabled? Check chat log\", \"Orange\")\n printToAll(\"It seems like you don't have 'Physics' fully enabled. From the top menu bar, open Options \u003e Physics and select 'Full' to experience this mod with all features.\")\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/PhysicsDetector\", function(require, _LOADED, __bundle_register, __bundle_modules)\n-- will notify the user to enable physics if it appears to not be fully enabled\n\n-- this event should only fire if physics aren't fully enabled\nfunction onCollisionExit()\n broadcastToAll(\"Physics disabled? Check chat log\", \"Orange\")\n printToAll(\"It seems like you don't have 'Physics' fully enabled. From the top menu bar, open Options \u003e Physics and select 'Full' to experience this mod with all features.\")\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/PhysicsDetector\")\nend)\nreturn __bundle_require(\"__root\")", "LuaScriptState": "", "MeasureMovement": false, "Name": "BlockSquare", @@ -210185,14 +147634,33 @@ }, "Autoraise": true, "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 }, - "Description": "Thanks for downloading Arkham SCE 3.8.0!\n\nAdded Parallel Rex Murphy to the deck importer!\r\nPlaymat slots are now editable via a new button!\nThe options panel has been updated with a new look.\r\nEdge of the Earth weaknesses now spawn with a single Damage or Horror token as a reminder of how they can be removed.\r", + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", "DragSelectable": true, "GMNotes": "", - "GUID": "bd6b3e", + "GUID": "834ad5", "Grid": true, "GridProjection": false, "Hands": false, @@ -210200,116 +147668,776 @@ "IgnoreFoW": false, "LayoutGroupSortIndex": 0, "Locked": false, - "LuaScript": "", - "LuaScriptState": "", + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "Arkham SCE 3.8.0 - 5/12/2024 - Page 1", + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", "Snap": true, - "States": { - "2": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "Draw Encounter button now draws to the draw area on left-click, and to the rightmost empty threat area slot on right-click.\nCard states can now be changed with number keys.\r\nCleanup Helper now resets activation tokens.\r\nThe deck importer now can import Hemlock Vale story assets.\r\nHotkey Reference card has been updated.", - "DragSelectable": true, - "GMNotes": "", - "GUID": "4a2b72", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "Arkham SCE 3.8.0 - 5/12/2024 - Page 2", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -27, - "posY": 1.55149889, - "posZ": -56.165, - "rotX": 4.07705976e-8, - "rotY": 90.00001, - "rotZ": 4.08891943e-9, - "scaleX": 3, - "scaleY": 1, - "scaleZ": 3 - }, - "Value": 0, - "XmlUI": "" - }, - "3": { - "AltLookAngle": { - "x": 0, - "y": 0, - "z": 0 - }, - "Autoraise": true, - "ColorDiffuse": { - "b": 1, - "g": 1, - "r": 1 - }, - "Description": "Nkosi Mabati helper updated with custom token compatability.\nNew right-click option has been added Nepthys.\nRite of Sanctification no longer has to be right-clicked for each token sealed.\nFixed Ancient Covenant and Blasphemous Covenant having low-quality images.\nMyriad other under-the-hood code optimizations and bugfixes.", - "DragSelectable": true, - "GMNotes": "", - "GUID": "b27cd3", - "Grid": true, - "GridProjection": false, - "Hands": false, - "HideWhenFaceDown": false, - "IgnoreFoW": false, - "LayoutGroupSortIndex": 0, - "Locked": false, - "LuaScript": "", - "LuaScriptState": "", - "MeasureMovement": false, - "Name": "Notecard", - "Nickname": "Arkham SCE 3.8.0 - 5/12/2024 - Page 3", - "Snap": true, - "Sticky": true, - "Tooltip": true, - "Transform": { - "posX": -27, - "posY": 1.55149889, - "posZ": -56.165, - "rotX": 5.014709e-8, - "rotY": 90.00001, - "rotZ": 3.128093e-8, - "scaleX": 3, - "scaleY": 1, - "scaleZ": 3 - }, - "Value": 0, - "XmlUI": "" - } - }, "Sticky": true, + "Tags": [ + "UniversalToken" + ], "Tooltip": true, "Transform": { - "posX": -27, - "posY": 1.551, - "posZ": -56.165, + "posX": -53.2, + "posY": 1.55, + "posZ": 9.477, "rotX": 0, - "rotY": 90, + "rotY": 270, "rotZ": 0, - "scaleX": 3, + "scaleX": 0.45, "scaleY": 1, - "scaleZ": 3 + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "a84ae4", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -53.2, + "posY": 1.55, + "posZ": 8.384, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "762df8", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -53.2, + "posY": 1.55, + "posZ": 7.291, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "589c7d", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -36.973, + "posY": 1.55, + "posZ": 24.8, + "rotX": 0, + "rotY": 0, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "642585", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -38.066, + "posY": 1.55, + "posZ": 24.8, + "rotX": 0, + "rotY": 0, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "6441b3", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -39.159, + "posY": 1.55, + "posZ": 24.8, + "rotX": 0, + "rotY": 0, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "f2d25a", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -53.2, + "posY": 1.55, + "posZ": -22.723, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "a5476b", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -53.2, + "posY": 1.55, + "posZ": -23.816, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "06ee01", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -53.2, + "posY": 1.55, + "posZ": -24.909, + "rotX": 0, + "rotY": 270, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "88d9ff", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -23.727, + "posY": 1.55, + "posZ": -24.8, + "rotX": 0, + "rotY": 180, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "42ec66", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -21.541, + "posY": 1.55, + "posZ": -24.8, + "rotX": 0, + "rotY": 180, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 + }, + "Value": 0, + "XmlUI": "" + }, + { + "AltLookAngle": { + "x": 0, + "y": 0, + "z": 0 + }, + "Autoraise": true, + "ColorDiffuse": { + "b": 0.43922, + "g": 0.43137, + "r": 0.42353 + }, + "CustomImage": { + "CustomTile": { + "Stackable": false, + "Stretch": true, + "Thickness": 0.1, + "Type": 2 + }, + "ImageScalar": 1, + "ImageSecondaryURL": "", + "ImageURL": "http://cloud-3.steamusercontent.com/ugc/2447222612020428918/898E79CD6752EE225ED8563EBCFFC09FF4566EE2/", + "WidthScale": 0 + }, + "CustomUIAssets": [ + { + "Name": "Bundle", + "Type": 1, + "URL": "http://cloud-3.steamusercontent.com/ugc/2447222612042389728/6D8D0B4A101882FC81B09F2ED6F3A206A9216A07/" + } + ], + "Description": "Action / Ability Token", + "DragSelectable": true, + "GMNotes": "", + "GUID": "f94579", + "Grid": true, + "GridProjection": false, + "Hands": false, + "HideWhenFaceDown": false, + "IgnoreFoW": false, + "LayoutGroupSortIndex": 0, + "Locked": false, + "LuaScript": "-- Bundled by luabundle {\"version\":\"1.6.0\"}\nlocal __bundle_require, __bundle_loaded, __bundle_register, __bundle_modules = (function(superRequire)\n\tlocal loadingPlaceholder = {[{}] = true}\n\n\tlocal register\n\tlocal modules = {}\n\n\tlocal require\n\tlocal loaded = {}\n\n\tregister = function(name, body)\n\t\tif not modules[name] then\n\t\t\tmodules[name] = body\n\t\tend\n\tend\n\n\trequire = function(name)\n\t\tlocal loadedModule = loaded[name]\n\n\t\tif loadedModule then\n\t\t\tif loadedModule == loadingPlaceholder then\n\t\t\t\treturn nil\n\t\t\tend\n\t\telse\n\t\t\tif not modules[name] then\n\t\t\t\tif not superRequire then\n\t\t\t\t\tlocal identifier = type(name) == 'string' and '\\\"' .. name .. '\\\"' or tostring(name)\n\t\t\t\t\terror('Tried to require ' .. identifier .. ', but no such module has been registered')\n\t\t\t\telse\n\t\t\t\t\treturn superRequire(name)\n\t\t\t\tend\n\t\t\tend\n\n\t\t\tloaded[name] = loadingPlaceholder\n\t\t\tloadedModule = modules[name](require, loaded, register, modules)\n\t\t\tloaded[name] = loadedModule\n\t\tend\n\n\t\treturn loadedModule\n\tend\n\n\treturn require, loaded, register, modules\nend)(nil)\n__bundle_register(\"__root\", function(require, _LOADED, __bundle_register, __bundle_modules)\nrequire(\"core/UniversalActionAbilityToken\")\nend)\n__bundle_register(\"core/UniversalActionAbilityToken\", function(require, _LOADED, __bundle_register, __bundle_modules)\nlocal class, symbol\n\nlocal listOfClasses = {\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal listOfClassDisplayNames = {\n \"Blue (Guardian)\",\n \"Purple (Mystic)\",\n \"Grey (Neutral)\",\n \"Green (Rogue)\",\n \"Orange (Seeker)\",\n \"Red (Survivor)\"\n}\n\nlocal listOfSymbols = {\n \"Activate\",\n \"Engage\",\n \"Evade\",\n \"Explore\",\n \"Fight\",\n \"FreeTrigger\",\n \"Investigate\",\n \"Move\",\n \"None\",\n \"Parley\",\n \"PlayItem\",\n \"Reaction\",\n \"Resource\",\n \"Scan\",\n \"Spell\",\n \"Tome\",\n \"Guardian\",\n \"Mystic\",\n \"Neutral\",\n \"Rogue\",\n \"Seeker\",\n \"Survivor\"\n}\n\nlocal colorsForClasses = {\n Guardian = Color.new(19 / 255, 84 / 255, 165 / 255),\n Mystic = Color.new(82 / 255, 18 / 255, 97 / 255),\n Neutral = Color.new(108 / 255, 110 / 255, 112 / 255),\n Rogue = Color.new(17 / 255, 72 / 255, 54 / 255),\n Seeker = Color.new(215 / 255, 115 / 255, 35 / 255),\n Survivor = Color.new(190 / 255, 30 / 255, 45 / 255)\n}\n\nfunction onSave()\n return JSON.encode({ class = class, symbol = symbol })\nend\n\nfunction onLoad(savedData)\n local loadedData = JSON.decode(savedData) or {}\n class = loadedData.class or \"Neutral\"\n symbol = loadedData.symbol or \"Neutral\"\n\n updateDisplay()\n addContextMenu()\n\n -- get random seed from Global so all are different\n math.randomseed(Global.call(\"getRandomSeed\"))\nend\n\nfunction updateDisplay()\n local xml = {\n -- background on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. class .. \"Background\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.1\",\n rotation = \"0 0 180\"\n }\n },\n -- ring on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.2\",\n rotation = \"0 0 180\"\n }\n },\n -- symbol on the front\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n height = \"200\",\n width = \"200\",\n position = \"0 0 -10.3\",\n rotation = \"0 0 180\"\n }\n },\n -- background on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/NeutralBackground\",\n height = \"000\",\n width = \"200\",\n position = \"0 0 0.1\",\n rotation = \"0 180 180\"\n }\n },\n -- ring on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/Ring\",\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.2\",\n rotation = \"0 180 180\"\n }\n },\n -- symbol on the back\n {\n tag = \"Image\",\n attributes = {\n image = \"Bundle/\" .. symbol,\n color = \"#000000\",\n height = \"200\",\n width = \"200\",\n position = \"0 0 0.3\",\n rotation = \"0 180 180\"\n }\n }\n }\n\n -- handling for double symbols\n if string.contains(symbol, \"/\") then\n local symbols = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbols, str)\n end\n\n -- update front image\n xml[3].attributes.image = \"Bundle/\" .. symbols[1]\n xml[3].attributes.height = xml[3].attributes.height * 0.55\n xml[3].attributes.width = xml[3].attributes.width * 0.55\n xml[3].attributes.position = \"35 0 -10.3\"\n\n -- add 2nd image element to front\n local frontSymbolXml = deepcopy(xml[3])\n frontSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n frontSymbolXml.attributes.position = \"-35 0 -10.3\"\n table.insert(xml, frontSymbolXml)\n\n -- update back image\n xml[6].attributes.image = \"Bundle/\" .. symbols[1]\n xml[6].attributes.height = xml[6].attributes.height * 0.55\n xml[6].attributes.width = xml[6].attributes.width * 0.55\n xml[6].attributes.position = \"35 0 0.3\"\n\n -- add 2nd image element to back\n local backSymbolXml = deepcopy(xml[6])\n backSymbolXml.attributes.image = \"Bundle/\" .. symbols[2]\n backSymbolXml.attributes.position = \"-35 0 0.3\"\n table.insert(xml, backSymbolXml)\n end\n\n self.UI.setXmlTable(xml)\n\n -- set color tint\n self.setColorTint(colorsForClasses[class])\n\n -- update name (only show symbol name if it isn't the class name)\n if isClassName(symbol) then\n self.setName(class)\n else\n self.setName(class .. \" \" .. symbol)\n end\n\n -- update scale\n if symbol == \"FreeTrigger\" or symbol == \"Reaction\" then\n self.setScale({ 0.35, 1, 0.35 })\n else\n self.setScale({ 0.45, 1, 0.45 })\n end\nend\n\nfunction getSymbolList()\n local symbolList = {}\n for str in string.gmatch(symbol, \"([^/]+)\") do\n table.insert(symbolList, str)\n end\n return symbolList\nend\n\nfunction addContextMenu()\n self.addContextMenuItem(\"Change color\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local currentClassDisplayName = listOfClassDisplayNames[getIndexOfValue(listOfClasses, class)]\n Player[playerColor].showOptionsDialog(\"Choose color\", listOfClassDisplayNames, currentClassDisplayName,\n function(_, selectedIndex)\n updateClass(listOfClasses[selectedIndex])\n end)\n end)\n\n self.addContextMenuItem(\"Change 1st symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose symbol\", listOfSymbols, symbolList[1], function(newSymbol)\n if newSymbol == \"None\" then\n if symbolList[2] then\n updateSymbol(symbolList[2])\n else\n printToColor(\"Need to have at least one symbol.\", playerColor)\n end\n else\n if symbolList[2] then\n updateSymbol(newSymbol .. \"/\" .. symbolList[2])\n else\n updateSymbol(newSymbol)\n end\n end\n end)\n end)\n\n self.addContextMenuItem(\"Change 2nd symbol\", function(playerColor)\n Player[playerColor].clearSelectedObjects()\n local symbolList = getSymbolList()\n Player[playerColor].showOptionsDialog(\"Choose 2nd symbol\", listOfSymbols, symbolList[2] or symbolList[1],\n function(newSymbol)\n if newSymbol == \"None\" then\n updateSymbol(symbolList[1])\n else\n updateSymbol(symbolList[1] .. \"/\" .. newSymbol)\n end\n end)\n end)\nend\n\nfunction updateClass(newClass)\n -- also update the symbol if it matches the class\n if class == symbol then\n symbol = newClass or \"Neutral\"\n end\n class = newClass or \"Neutral\"\n updateDisplay()\nend\n\nfunction updateSymbol(newSymbol)\n symbol = newSymbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction updateClassAndSymbol(params)\n class = params.class or \"Neutral\"\n symbol = params.symbol or class\n\n if symbol == \"Universal\" then\n symbol = class\n end\n updateDisplay()\nend\n\nfunction isClassName(str)\n for _, className in ipairs(listOfClasses) do\n if className == str then\n return true\n end\n end\n return false\nend\n\nfunction onRandomize()\n local newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n\n while newSymbol == \"None\" do\n newSymbol = listOfSymbols[math.random(1, #listOfSymbols)]\n end\n\n -- if the new symbol is a class symbol, don't get a random class\n if isClassName(newSymbol) then\n updateClassAndSymbol({ class = newSymbol, symbol = newSymbol })\n else\n updateClassAndSymbol({ class = listOfClasses[math.random(1, #listOfClasses)], symbol = newSymbol })\n end\nend\n\nfunction getIndexOfValue(t, value)\n for i, v in ipairs(t) do\n if v == value then\n return i\n end\n end\n return nil\nend\n\nfunction deepcopy(orig)\n local copy\n if type(orig) == 'table' then\n copy = {}\n for orig_key, orig_value in next, orig, nil do\n copy[deepcopy(orig_key)] = deepcopy(orig_value)\n end\n setmetatable(copy, deepcopy(getmetatable(orig)))\n else -- number, string, boolean, etc\n copy = orig\n end\n return copy\nend\nend)\nreturn __bundle_require(\"__root\")", + "LuaScriptState": "{\"class\":\"Neutral\",\"symbol\":\"Neutral\"}", + "MeasureMovement": false, + "Memo": "universalActionAbility", + "Name": "Custom_Tile", + "Nickname": "Neutral", + "Snap": true, + "Sticky": true, + "Tags": [ + "UniversalToken" + ], + "Tooltip": true, + "Transform": { + "posX": -22.634, + "posY": 1.55, + "posZ": -24.8, + "rotX": 0, + "rotY": 180, + "rotZ": 0, + "scaleX": 0.45, + "scaleY": 1, + "scaleZ": 0.45 }, "Value": 0, "XmlUI": "" @@ -210324,7 +148452,7 @@ 0, 0 ], - "SaveName": "Arkham SCE - 3.8.0", + "SaveName": "Arkham SCE - 3.9.0", "Sky": "Sky_Museum", "SkyURL": "https://i.imgur.com/GkQqaOF.jpg", "SnapPoints": [ @@ -210643,46 +148771,48 @@ "y": 1.481, "z": -87 } + }, + { + "Position": { + "x": 60, + "y": 1.481, + "z": 48 + } + }, + { + "Position": { + "x": -42, + "y": 1.481, + "z": 89 + } + }, + { + "Position": { + "x": -42, + "y": 1.481, + "z": 71 + } + }, + { + "Position": { + "x": -62, + "y": 1.481, + "z": 71 + } + }, + { + "Position": { + "x": -62, + "y": 1.481, + "z": 89 + } } ], "TabStates": { - "10": { - "body": "Created by Whimsical\n\nAnything that passes over the remover that isn't a card, deck or chaos token will be deleted.\r\nTo use the remover, right click on it, choose the \"Enable\" option, and take your card with resources/horror/damage and swipe it over the remover. You may wish to unlock and/or copy the remover to your play area first.", + "1": { + "body": "Welcome to Arkham Horror LCG - Super Complete Edition!\n\nMake sure to take the tour that can be started with the token in the middle of the main playarea. Some basic notes:\n\nDECKBUILDING\n- All currently existing investigators and player cards are accessible via the player card panel in the upper left corner of the table.\n\n- On the leftside underneath the Investigators, you will find the ArkhamDB Deckimporter. Insert your deck ID and it will build the deck automatically for you.\n\nSCENARIOS \u0026 SETUP\n- Arkham Horror LCG comes with a core campaign (Night of the Zealot) and several expansions. Within each box you will find all the cards required for each scenario setup, as well as a the official campaign guide PDF.\n\n2. Each scenario is setup differently, and while some of the work has been prepared beforehand (such as building encounter decks), you will have to refer to the Campaign Guide for specific instructions on how to set up each scenario.\n\nINVESTIGATOR PLAYMAT AND GAMEPLAY\n- Playermats are scripted to automate most of the gameplay for you.", "color": "Grey", - "id": 10, - "title": "Token Remover", - "visibleColor": { - "b": 0.5, - "g": 0.5, - "r": 0.5 - } - }, - "11": { - "body": "By Whimsical. Requires Numlock set to On.\nNumpad 1: Cut top 3 cards of deck\nNumpad 2: Cut top 6 cards of deck\nNumpad 3: Cut top 9 cards of deck\nNumpad 4: Spawn Damage\nNumpad 5: Spawn Connection Marker\nNumpad 6: Spawn Horror\nNumpad 7: Spawn Doom\nNumpad 8: Spawn Clue\nNumpad 9: Spawn Resource\nNumpad 0: Draw lines between selected objects. Hold to draw lines from mouseover object to other selected objects.", - "color": "Grey", - "id": 11, - "title": "Numpad Hotkeys", - "visibleColor": { - "b": 0.5, - "g": 0.5, - "r": 0.5 - } - }, - "7": { - "body": "The server host can enable or disable cards in hands being hidden from other players by going to the menu at the top of the TTS screen, clicking options, and choosing Hands. The \"Disable\" setting reveals all player hands to all players, while the \"Default\" setting means that each player can only see the cards in their own hand.", - "color": "Grey", - "id": 7, - "title": "How to Hide Hands", - "visibleColor": { - "b": 0.5, - "g": 0.5, - "r": 0.5 - } - }, - "8": { - "body": "Welcome to Arkham Horror LCG - Super Complete Edition!\r\n\r\nBelow you will find all the features and instructions this mod is loaded with, that will make your AH LCG experience easier.\r\n\r\nDECKBUILDING\r\n1. All current existing investigators are on the right-hand side, and within each chest you will find their investigator-specific assets and weaknesses. Also included is a basic starter deck which only requires you to add a basic random weakness to get going.\r\n\r\n2. On the left-hand side you will find both the weakness decks as well as lvl 0 cards sorted by class. To reveal the cards, click on each corresponding token to deal the cards onto the table. Cards are sorted by order of Skill, event and Asset cards top-down and increasing resource cost, left to right. \r\n\r\n3. On the upper side you have the upgrade cards. Similarly, click each token to deal the cards out onto the table. Cards are arranged both in increasing xp cost and resource cost, left to right. Typically, these are the cards you will be spending XP on between scenarios to purchase and improve your deck.\r\n4. On the right-hand side underneath the Investigators, you will find the automated ArkhamDB Deckbuilder (coded and maintained by Grabben). Click the load cards button to activate the Deckbuilder, check ArkhamDB for your chosen deck’s URL and insert its number code following the instructions on the deckbuilder, and it will build the deck automatically for you.\r\n\r\nSCENARIOS \u0026 SETUP\r\n1. \tArkham Horror LCG comes with a core campaign (Night of the Zealot) and several expansions (The Dunwich Legacy, The Path to Carcosa \u0026 The Forgotten Age). Within each box you will find the volumes that contain all the cards required for each scenario setup, as well as a tablet linking to the official campaign guide PDF. Also included are chaos token cards and a Campaign Log.\r\n\r\n2. \tEach scenario is setup differently, and while some of the work has been prepared beforehand (such as building encounter decks), you will have to refer to the Campaign Guide for specific instructions on how to set up each scenario.\r\n\r\n3. \tThe chaos bag is always placed on the scenario setup mat in the upper right-hand corner onto a snap point that tilts it at a 45-degree angle. Each scenario volume will contain a difficulty card, where you will have the choice of four difficulties. Press the button on the card according to the difficulty of your choosing and the chaos bag will automatically be configured with the tokens specific to that difficulty. In campaign play it is recommended to save your decks and chaos bag at the end of your scenario to carry over onto the next, as often tokens are added or removed from the chaos bag depending on actions or decisions made during the game. These additional tokens can be drawn from the token reserve book resting next to the newspaper in the middle of the main table – right click it and search for the token you need.\n\r\n4. \tWhen placing location cards, always place them face down on the main play area (the dark map of Arkham, Massachusetts) with the number of clues per location unrevealed. The mod is scripted so that when you flip said location cards (usually when entering the location with an investigator), if it contains clues, the number of clues specific to that location will automatically spawn. Note, that the mod only spawns tokens in relation to the number of players currently set on the playmat player settings. To set the number of players, left-click on the \"Investigators Playing\" number to increase, or right-click to decrease.\r\n\n5. To make location mapping easier, you can draw location connector tokens from the arrow-shaped container below the main play area. Each token has three states (one way, two way and four way) to use them accordingly to better visualize how your locations connect.\n\r\n6. \tIf you require additional doom or clue tokens, these are located on the scenario playmat in their corresponding containers. A handy Doom counter has been also been added to track the doom on the agenda – left-click to add to add, and right-click to deduct. Keep in mind that any doom spawned on enemies, locations or assets needs to be mentally added to the doom in play on the agenda to account for total doom.\r\n\r\nINVESTIGATOR PLAYMAT AND GAMEPLAY\r\n1. \tInvestigator mats are scripted to automate most of the gameplay for you. wdw\n\r\n2. \tEach mat has slots for inventory, where if you play an asset (for example you put a gun that has 4 ammo into your right-hand slot), the mod will automatically spawn the 4 resource tokens onto your equipped card.\n\r\n3.\tThe draw encounter button on the left-hand side will draw the topmost card from the encounter deck and put it in your threat area. Left-clicking will draw the card face-down, and right-clicking will draw the card face-up. When you draw a weakness, or engage an enemy, it is recommended you put it in your threat area, and once you defeat the enemy or treachery, you can send it to the encounter discard pile by clicking the discard button. If you defeat an enemy with a victory point, make sure not to discard to the discard pile, but pick the card up and drop it at the victory display.\n\r\n4. \tThe Click for Chaos button does just that, draw a random chaos token from your chaos bag. Clicking a second time, sends your chaos token back into the bag, which is then shuffled. If one player clicks to draw a token and doesn’t click a second time to send it back, the click from another player on his personal mat will send the token back first, and the next click will draw the token. Additionally, right-clicking the button will continue drawing tokens and line them up next to each other, which is useful for specific draw conditions the game may require from you. Left-clicking again will send all drawn tokens back to the bag.\n\nADDITIONAL FEATURES:\n1. Over 20 Fan-made scenarios created by the thriving community of Arkham Horror LCG have been included. Some of these are one-scenario missions, others are long involved campaigns spanning multiple scenarios. These are all contained in \"The Side Missions\". This boxset also includes the official FFG-created sidemissions Curse of the Rougarou, Carnevale of Horrors, Labrynths of Lunacy and The Eternal Slumber. Read the rulebook on including a side-mission into an ongoing campaign, or play it as a one-off adventure! Setup instructions are included in each volume.\n\n2. If you are not a fan of the dark themed Arkham map for the playmat, you can change the image on it to any you like. At the top left hand side of the playmat is an image icon, which when clicked will reveal a image swap panel. Input the URL for the image you want to repalce the playmat with, and the panel will apply the image for you. Keep in mind this will not change the existing snap points on the current playmat.\n\r\nAs a final comment, please be sure to let me know on the mod page in steam workshop if you find any bugs, issues or have any suggestions for improvement!\r\n\r\n\r\n \r\n\r\n", - "color": "Grey", - "id": 8, + "id": 1, "title": "Basic Intro", "visibleColor": { "b": 0.5, @@ -210690,11 +148820,11 @@ "r": 0.5 } }, - "9": { - "body": "Implemented by Tikatoy\nIdea conceived by Cadentia\n\nVersion 3.3\n\nTop buttons manage bless tokens, bottom buttons manage curse tokens\nADD - creates a new token and adds it to the chaos bag\nREMOVE - removes a token from the chaos bag and destroys it\nTAKE - takes a token from the chaos bag and places it below the manager (for sealing)\nRETURN - returns the last token taken from the chaos bag to the chaos bag\n\nTo use Parallel Wendy, go to Options -\u003e Game Keys, then bind a key or mouse button to Wendy's Menu. Hover over any card (won't work on decks) then press the bound key. Right-click seal/release options will be added to the card.\n\n---Other Notes---\n\nOnly use ONE token manager at a time\nTokens are limited to 10 of each type in play\nBless and curse tokens should be in the chaos bag before trying to REMOVE or TAKE them\nEach action logs a message which ends with (# in bag/# taken); hit enter to view log\n**WARNING**: Tracking # of tokens in bag and in play will NOT persist between saves\n\r", + "2": { + "body": "The server host can enable or disable cards in hands being hidden from other players by going to the menu at the top of the TTS screen, clicking options, and choosing Hands. The \"Disable\" setting reveals all player hands to all players, while the \"Default\" setting means that each player can only see the cards in their own hand.", "color": "Grey", - "id": 9, - "title": "Bless / Curse Manager", + "id": 2, + "title": "How to Hide Hands", "visibleColor": { "b": 0.5, "g": 0.5, @@ -210715,5 +148845,5 @@ "Type": 0 }, "VersionNumber": "v13.2.2", - "XmlUI": "\u003c!-- include Global/Global.xml --\u003e\n\u003cDefaults\u003e\n \u003c!-- general stuff --\u003e\n \u003cText color=\"white\"\n fontSize=\"18\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- include Global/BottomBar.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton class=\"navbar\"\n tooltipPosition=\"Left\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\n color=\"clear\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- Buttons at the bottom right (height: n * 37 - 2) --\u003e\n\u003cVerticalLayout visibility=\"Admin\"\n color=\"#000000\"\n outlineSize=\"1 1\"\n outline=\"#303030\"\n rectAlignment=\"LowerRight\"\n width=\"35\"\n height=\"72\"\n offsetXY=\"-1 120\"\n spacing=\"2\"\u003e\n \u003cButton class=\"navbar\"\n icon=\"devourer\"\n tooltip=\"Downloadable Content\"\n onClick=\"onClick_toggleUi(downloadWindow)\"/\u003e\n \u003cButton class=\"navbar\"\n icon=\"option-gear\"\n tooltip=\"Options\"\n onClick=\"onClick_toggleUi(optionPanel)\"/\u003e\n\u003c/VerticalLayout\u003e\n\n\u003c!-- Navigation Overlay button (not visibly to Grey and Black) --\u003e\n\u003cPanel visibility=\"White|Brown|Red|Orange|Yellow|Green|Teal|Blue|Purple|Pink\"\n color=\"#000000\"\n outlineSize=\"1 1\"\n outline=\"#303030\"\n rectAlignment=\"LowerRight\"\n width=\"35\"\n height=\"35\"\n offsetXY=\"-1 85\"\u003e\n \u003cButton class=\"navbar\"\n icon=\"NavigationOverlayIcon\"\n tooltip=\"Navigation Overlay\"\n onClick=\"onClick_toggleUi(Navigation Overlay)\"/\u003e\n\u003c/Panel\u003e\n\u003c!-- include Global/BottomBar.xml --\u003e\n\u003c!-- include Global/DownloadWindow.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton class=\"downloadTab\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n onClick=\"onClick_tab\"\n color=\"#888888\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"bGrey\"\n color=\"grey\"/\u003e\n \u003cButton class=\"bWhite\"\n color=\"white\"/\u003e\n \u003cButton class=\"activeTab\"\n color=\"#ffffff\"/\u003e\n \u003cButton class=\"windowButton\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n selectClass=\"bWhite\"\n color=\"#888888\"\n font=\"font_teutonic-arkham\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- window to select downloadable content --\u003e\n\u003cVerticalLayout id=\"downloadWindow\"\n color=\"black\"\n active=\"false\"\n height=\"800\"\n width=\"900\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\u003e\n\n \u003c!-- window header --\u003e\n \u003cPanel preferredHeight=\"60\"\n padding=\"10 10 5 5\"\n spacing=\"10\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\n color=\"black\"\u003e\n \u003cText fontSize=\"32\"\n font=\"font_teutonic-arkham\"\n preferredWidth=\"600\"\n alignment=\"MiddleLeft\"\u003eDownloadable Content\u003c/Text\u003e\n \u003cButton id=\"downloadAll_button\"\n class=\"windowButton\"\n visibility=\"Black\"\n onClick=\"onClick_downloadAll\"\n height=\"30\"\n preferredWidth=\"110\"\n fontSize=\"20\"\n tooltip=\"Very rough estimate: 400 MB\"\n tooltipPosition=\"Above\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\u003eDownload Everything\u003c/Button\u003e\n \u003cButton id=\"spawnPlaceholder_button\"\n class=\"windowButton\"\n visibility=\"Black\"\n onClick=\"onClick_spawnPlaceholder\"\n height=\"30\"\n preferredWidth=\"110\"\n fontSize=\"20\"\u003eSpawn Placeholder\u003c/Button\u003e\n \u003cPanel preferredWidth=\"50\"\u003e\n \u003cButton rectAlignment=\"MiddleRight\"\n width=\"50\"\n color=\"clear\"\n icon=\"close\"\n tooltip=\"Close\"\n tooltipPosition=\"Right\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\n onClick=\"onClick_toggleUi(downloadWindow)\"/\u003e\n \u003c/Panel\u003e\n \u003c/Panel\u003e\n\n \u003cHorizontalLayout\u003e\n \u003cVerticalLayout preferredWidth=\"600\"\u003e\n \u003c!-- tab selection --\u003e\n \u003cHorizontalLayout preferredHeight=\"60\"\n padding=\"5\"\n spacing=\"5\"\u003e\n \u003cButton class=\"downloadTab activeTab\"\n id=\"tab1\"\u003eOfficial Campaigns\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab2\"\u003eOfficial Scenarios\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab3\"\u003eFan-Made Campaigns\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab4\"\u003eFan-Made Scenarios\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab5\"\u003eFan-Made Player Cards\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n\n \u003c!-- content list --\u003e\n \u003cVerticalScrollView color=\"transparent\"\n minHeight=\"100\"\n flexibleHeight=\"100\"\n scrollSensitivity=\"27\"\n scrollbarColors=\"grey|grey|#C8C8C8|rgba(0.78,0.78,0.78,0.5)\"\n horizontalScrollbarVisibility=\"AutohideAndExpandViewport\"\n raycastTarget=\"true\"\u003e\n \u003cVerticalLayout id=\"contentList\"\n padding=\"10 25 0 0\"\u003e\n \u003c!-- this will be filled via scripting --\u003e\n \u003c/VerticalLayout\u003e\n \u003c/VerticalScrollView\u003e\n \u003c/VerticalLayout\u003e\n\n \u003c!-- content preview window --\u003e\n \u003cVerticalLayout preferredWidth=\"300\"\n padding=\"15 15 15 5\"\u003e\n\n \u003c!-- header --\u003e\n \u003cVerticalLayout preferredHeight=\"110\"\u003e\n \u003cText id=\"previewTitle\"\n fontSize=\"28\"\n preferredHeight=\"70\"\n font=\"font_teutonic-arkham\"\u003ePreviewTitle\u003c/Text\u003e\n \u003cText id=\"previewAuthor\"\n fontSize=\"20\"\n preferredHeight=\"40\"\n font=\"font_teutonic-arkham\"\u003eby PreviewAuthor\u003c/Text\u003e\n \u003c/VerticalLayout\u003e\n\n \u003c!-- box art --\u003e\n \u003cPanel id=\"previewArtPanel\"\n preferredHeight=\"390\"\u003e\n \u003cMask id=\"previewArtMask\"\u003e\n \u003c!-- image will be set via scripting --\u003e\n \u003cImage id=\"previewArtImage\" /\u003e\n \u003c/Mask\u003e\n \u003c/Panel\u003e\n\n \u003c!-- description --\u003e\n \u003cPanel preferredHeight=\"160\"\u003e\n \u003cText id=\"previewDescription\"\n alignment=\"UpperLeft\"\n resizeTextForBestFit=\"true\"\n resizeTextMinSize=\"12\"\n resizeTextMaxSize=\"18\"\u003ePreviewDescription\u003c/Text\u003e\n \u003c/Panel\u003e\n\n \u003c!-- download button / progress bar (visibility handled by lua code)--\u003e\n \u003cPanel preferredHeight=\"60\"\u003e\n \u003c!-- download button --\u003e\n \u003cButton id=\"download_button\"\n class=\"windowButton\"\n onClick=\"onClick_download\"\n height=\"50\"\n width=\"270\"\n fontSize=\"28\"\u003eDownload\u003c/Button\u003e\n \u003c!-- download progress bar --\u003e\n \u003cProgressBar id=\"download_progress\"\n active=\"false\"\n height=\"50\"\n width=\"270\"\n percentage=\"0\"\n color=\"#111111\"\n textColor=\"#aaaaaa\"\n fillImageColor=\"#333333\"/\u003e\n \u003c/Panel\u003e\n \u003c/VerticalLayout\u003e\n \u003c/HorizontalLayout\u003e\n\u003c/VerticalLayout\u003e\n\u003c!-- include Global/DownloadWindow.xml --\u003e\n\u003c!-- include Global/PlayAreaGallery.xml --\u003e\n\u003cDefaults\u003e\n \u003c!-- type selection at the top --\u003e\n \u003cButton class=\"imageTab\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n onClick=\"b7b45b/onClick_imageTab\"\n color=\"#888888\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"bGrey\"\n color=\"grey\"/\u003e\n \u003cButton class=\"bWhite\"\n color=\"white\"/\u003e\n\n \u003cButton class=\"windowButton\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n selectClass=\"bWhite\"\n color=\"#888888\"\n font=\"font_teutonic-arkham\"/\u003e\n\n \u003c!-- image boxes in the grid --\u003e\n \u003cVerticalLayout class=\"imageBox\"\n color=\"black\"\n outline=\"#303030\"\n outlineSize=\"2 2\"\n onClick=\"b7b45b/onClick_image\"/\u003e\n \u003cImage class=\"playareaImage\"\n preferredHeight=\"330\"/\u003e\n \u003cText class=\"imageName\"\n preferredHeight=\"40\"\n resizeTextForBestFit=\"true\"\n resizeTextMinSize=\"10\"\n resizeTextMaxSize=\"18\"/\u003e\n\n \u003c!-- item selection on the left --\u003e\n \u003cText class=\"itemText\"\n alignment=\"MiddleLeft\"/\u003e\n \u003cPanel class=\"itemPanel\"\n preferredHeight=\"45\"\n onClick=\"b7b45b/onClick_listItem\"/\u003e\n\n \u003c!-- other --\u003e\n \u003cText class=\"headerText\"\n fontSize=\"35\"/\u003e\n \u003cVerticalLayout childForceExpandHeight=\"false\"/\u003e\n\u003c/Defaults\u003e\n\n\u003cVerticalLayout id=\"playAreaGallery\"\n active=\"false\"\n color=\"black\"\n height=\"880\"\n width=\"900\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\u003e\n\n \u003c!-- window header --\u003e\n \u003cPanel preferredHeight=\"60\"\n padding=\"10 10 5 5\"\n spacing=\"10\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\n color=\"black\"\u003e\n \u003cText fontSize=\"32\"\n font=\"font_teutonic-arkham\"\n preferredWidth=\"600\"\n alignment=\"MiddleLeft\"\u003ePlayarea Image Gallery\u003c/Text\u003e\n \u003cButton id=\"customUrl_button\"\n class=\"windowButton\"\n onClick=\"onClick_customUrl\"\n height=\"30\"\n preferredWidth=\"125\"\n fontSize=\"24\"\u003eUse custom URL\u003c/Button\u003e\n \u003cPanel preferredWidth=\"50\"\u003e\n \u003cButton rectAlignment=\"MiddleRight\"\n width=\"50\"\n color=\"clear\"\n icon=\"close\"\n tooltip=\"Close\"\n tooltipPosition=\"Right\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\n onClick=\"onClick_toggleUi(playAreaGallery)\"/\u003e\n \u003c/Panel\u003e\n \u003c/Panel\u003e\n\n \u003c!-- tab selection --\u003e\n \u003cHorizontalLayout preferredHeight=\"60\"\n padding=\"5\"\n spacing=\"5\"\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab1\"\u003eOfficial Campaigns\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab2\"\u003eOfficial Scenarios\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab3\"\u003eFan-Made Campaigns\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab4\"\u003eFan-Made Scenarios\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab5\"\u003eOther Images\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n\n \u003cHorizontalLayout preferredHeight=\"760\"\u003e\n \u003c!-- left column: navigation bar --\u003e\n \u003cVerticalLayout id=\"itemSelection\"\n preferredWidth=\"180\"\n padding=\"10 15 0 0\"\u003e\n \u003c!-- this will be filled via scripting --\u003e\n \u003c!-- \u003cPanel class=\"itemPanel\"\u003e\n \u003cText class=\"itemText\"\u003eItem\u003c/Text\u003e\n \u003c/Panel\u003e --\u003e\n \u003c/VerticalLayout\u003e\n\n \u003c!-- right column: image gallery --\u003e\n \u003cVerticalScrollView color=\"transparent\"\n minHeight=\"100\"\n flexibleHeight=\"100\"\n preferredWidth=\"720\"\n scrollSensitivity=\"380\"\n scrollbarColors=\"grey|grey|#C8C8C8|rgba(0.78,0.78,0.78,0.5)\"\n horizontalScrollbarVisibility=\"AutohideAndExpandViewport\"\n raycastTarget=\"true\"\u003e\n \u003cGridLayout id=\"playareaList\"\n preferredWidth=\"700\"\n padding=\"25 25 5 5\"\n spacing=\"10\"\n cellSize=\"330 370\"\u003e\n \u003c!-- this will be filled via scripting --\u003e\n \u003c!-- \u003cVerticalLayout class=\"imageBox\"\u003e\n \u003cImage class=\"playareaImage\" image=\"\"/\u003e\n \u003cText class=\"imageName\"\u003eImage Name\u003c/Text\u003e\n \u003c/VerticalLayout\u003e --\u003e\n \u003c/GridLayout\u003e\n \u003c/VerticalScrollView\u003e\n \u003c/HorizontalLayout\u003e\n\u003c/VerticalLayout\u003e\n\u003c!-- include Global/PlayAreaGallery.xml --\u003e\n\u003c!-- include Global/TitleSplash.xml --\u003e\n\u003c!-- Title Splash when starting a scenario --\u003e\n\u003cPanel id=\"title_splash\"\n height=\"220\"\n position=\"0 250 0\"\n showAnimation=\"FadeIn\"\n hideAnimation=\"FadeOut\"\n active=\"false\"\n animationDuration=\"2\"\u003e\n \u003cImage id=\"title_gradient\"\n height=\"220\"\n image=\"TitleGradient\" /\u003e\n \u003cText id=\"title_splash_text\"\n width=\"95%\"\n height=\"180\"\n resizeTextForBestFit=\"true\"\n resizeTextMinSize=\"100\"\n resizeTextMaxSize=\"150\"\n font=\"font_teutonic-arkham\"\n outline=\"black\"\n outlineSize=\"3 -3\"\n horizontalOverflow=\"Overflow\"\u003e\n \u003c/Text\u003e\n\u003c/Panel\u003e\n\u003c!-- include Global/TitleSplash.xml --\u003e\n\u003c!-- include Global/NavigationOverlay.xml --\u003e\n\u003c!-- Default formatting --\u003e\n\u003cDefaults\u003e\n \u003cText color=\"#FFFFFF\"\n alignment=\"MiddleLeft\" /\u003e\n\n \u003cToggle isOn=\"False\"\n rectAlignment=\"MiddleRight\" /\u003e\n\n \u003cCell dontUseTableCellBackground=\"true\"\n outlineSize=\"0 1\"\n outline=\"grey\" /\u003e\n\n \u003c!-- options --\u003e\n \u003cRow class=\"nav_option-text\"\n preferredHeight=\"45\"/\u003e\n \u003cCell class=\"nav_option-text\"\n color=\"#333333\"/\u003e\n \u003cCell class=\"nav_option-button\"\n color=\"#333333\"/\u003e\n \u003cText class=\"nav_option-header\"\n fontSize=\"20\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cCell class=\"claim\"\n tooltip=\"Clicking this seat in the navigation overlay\u0026#xA;will now only swap the playercolor for you.\"\n tooltipPosition=\"Right\" /\u003e\n\n \u003c!-- buttons at the bottom --\u003e\n \u003cButton class=\"bottomButtons\"\n hoverClass=\"hover\"\n pressClass=\"press\"\n selectClass=\"select\"\n color=\"#888888\"\n minHeight=\"35\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"hover\"\n color=\"grey\"/\u003e\n \u003cButton class=\"press\"\n color=\"white\"/\u003e\n \u003cButton class=\"select\"\n color=\"white\"/\u003e\n\n \u003c!-- Navigation Panels --\u003e\n \u003cPanel class=\"navPanel\"\n active=\"false\"\n allowDragging=\"true\"\n rectAlignment=\"LowerRight\"\n returnToOriginalPositionWhenReleased=\"false\"\n offsetXY=\"-40 0\"\u003e\n \u003c/Panel\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- full Panel --\u003e\n\u003cPanel id=\"navPanelFull\"\n height=\"358\"\n width=\"455\"\n class=\"navPanel\"\u003e\n\u003c/Panel\u003e\n\n\u003c!-- Play Area only --\u003e\n\u003cPanel id=\"navPanelPlay\"\n height=\"208\"\n width=\"205\"\n class=\"navPanel\"\u003e\n\u003c/Panel\u003e\n\n\u003c!-- Settings --\u003e\n\u003cTableLayout id=\"navPanelSettings\"\n active=\"false\"\n width=\"300\"\n height=\"380\"\n color=\"#000000\"\n outlineSize=\"2 2\"\n outline=\"grey\"\n rectAlignment=\"MiddleCenter\"\u003e\n\n \u003c!-- Header --\u003e\n \u003cRow preferredHeight=\"60\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"10 0 0 0\"\u003e\n \u003cText font=\"font_teutonic-arkham\"\n fontSize=\"35\"\u003eNavigation Overlay\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Options --\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cTableLayout columnWidths=\"0 125\"\n autoCalculateHeight=\"1\"\n cellPadding=\"10 0 5 5\"\u003e\n\n \u003c!-- Option: Custom pitch --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eViewing angle in degrees:\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button\"\u003e\n \u003cSlider id=\"sliderPitch\"\n onValueChanged=\"797ede/updatePitch\"\n wholeNumbers=\"true\"\n minValue=\"0\"\n maxValue=\"89\"\n value=\"75\"\n tooltip=\"This controls the camera pitch\u0026#xA;('nodding your head').\"\n tooltipPosition=\"Right\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Custom distance --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eViewing distance (relative):\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button\"\u003e\n \u003cSlider id=\"sliderDistance\"\n onValueChanged=\"797ede/updateDistance\"\n wholeNumbers=\"true\"\n minValue=\"50\"\n maxValue=\"200\"\n value=\"100\"\n tooltip=\"This controls the camera distance\u0026#xA;(from 50% to 200% of the default settings).\"\n tooltipPosition=\"Right\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim White --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"White\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimWhite\"\n onValueChanged=\"797ede/claimColor(White)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim Orange --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"Orange\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimOrange\"\n onValueChanged=\"797ede/claimColor(Orange)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim Green --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"Green\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimGreen\"\n onValueChanged=\"797ede/claimColor(Green)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim Red --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"Red\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimRed\"\n onValueChanged=\"797ede/claimColor(Red)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Buttons: Defaults and Close --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cHorizontalLayout minHeight=\"55\"\n flexibleHeight=\"0\"\n padding=\"10 10 5 10\"\n spacing=\"35\"\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"797ede/loadDefaultSettings\"\u003eLoad defaults\u003c/Button\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"797ede/toggleSettings\"\u003eClose\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include Global/NavigationOverlay.xml --\u003e\n\u003c!-- include Global/OptionPanel.xml --\u003e\n\u003c!-- Default formatting --\u003e\n\u003cDefaults\u003e\n \u003cText color=\"#FFFFFF\"\n alignment=\"MiddleLeft\"/\u003e\n\n \u003cDropdown rectAlignment=\"MiddleCenter\"/\u003e\n\n \u003cCell dontUseTableCellBackground=\"true\"\n outlineSize=\"0 1\"\n outline=\"grey\"/\u003e\n\n \u003c!-- main window --\u003e\n \u003cTableLayout class=\"window\"\n width=\"500\"\n height=\"800\"\n active=\"false\"\n color=\"#000000\"\n outlineSize=\"2 2\"\n outline=\"grey\"\n showAnimation=\"SlideIn_Right\"\n hideAnimation=\"SlideOut_Right\"\n animationDuration=\"0.2\"/\u003e\n\n \u003c!-- group headers --\u003e\n \u003cRow class=\"group-header\"\n preferredHeight=\"44\"/\u003e\n \u003cCell class=\"group-header\"\n padding=\"10 10 0 0\"\n columnSpan=\"3\"\n color=\"#222222\"/\u003e\n \u003cPanel class=\"group-header\"\n padding=\"5 0 0 0\"/\u003e\n \u003cText class=\"group-header\"\n fontSize=\"28\"\n font=\"font_teutonic-arkham\"/\u003e\n\n \u003c!-- options --\u003e\n \u003cRow class=\"option-text\"\n preferredHeight=\"50\"\n tooltipPosition=\"Left\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"/\u003e\n \u003cCell class=\"option-text\"\n padding=\"10 10 5 5\"\n color=\"#333333\"\n columnSpan=\"2\"/\u003e\n \u003cCell class=\"option-button\"\n padding=\"10 10 5 5\"\n color=\"#333333\"/\u003e\n \u003cCell class=\"option-singleColumn\"\n padding=\"10 10 5 5\"\n color=\"#333333\"\n columnSpan=\"1\"/\u003e\n \u003cCell class=\"option-doubleColumn\"\n padding=\"10 10 5 5\"\n color=\"#333333\"\n columnSpan=\"2\"/\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\n padding=\"10 0 0 0\"/\u003e\n \u003cText class=\"option-header\"\n fontSize=\"22\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\n padding=\"0 17 3 3\"/\u003e\n \u003cButton class=\"optionToggle\"\n image=\"option-off\"\n rectAlignment=\"MiddleRight\"\n offsetXY=\"-30 0\"\n colors=\"#FFFFFF|#dfdfdf\"\n height=\"36\"\n width=\"65\"\n ignoreLayout=\"True\"/\u003e\n\n \u003c!-- buttons at the bottom --\u003e\n \u003cButton class=\"bottomButtons\"\n hoverClass=\"hover\"\n pressClass=\"press\"\n selectClass=\"select\"\n color=\"#888888\"\n minHeight=\"35\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"hover\"\n color=\"grey\"/\u003e\n \u003cButton class=\"press\"\n color=\"white\"/\u003e\n \u003cButton class=\"select\"\n color=\"white\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- Option Panel --\u003e\n\u003cTableLayout id=\"optionPanel\"\n class=\"window\"\n rectAlignment=\"LowerRight\"\n offsetXY=\"-50 80\"\n raycastTarget=\"true\"\u003e\n \u003c!-- Header: Options --\u003e\n \u003cRow preferredHeight=\"60\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"10 0 0 0\"\u003e\n \u003cText font=\"font_teutonic-arkham\"\n fontSize=\"35\"\u003eOptions\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Scrollable part with options --\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cVerticalScrollView horizontalScrollbarVisibility=\"AutohideAndExpandViewport\"\n scrollSensitivity=\"30\"\n raycastTarget=\"true\"\u003e\n \u003cTableLayout columnWidths=\"0 100 75\"\n autoCalculateHeight=\"1\"\n useGlobalCellPadding=\"false\"\u003e\n\n \u003c!-- Group: general settings --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_acolyte\"\u003e\n \u003cText class=\"group-header\"\u003eGENERAL SETTINGS\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: card language --\u003e\n \u003c!-- disabled until we have the backend in place\n \u003cRow class=\"option-text\" tooltip=\"Downloading a campaign or importing a deck will use\u0026#xA;this language for cards (NOT FUNCTIONAL YET!).\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eCard language\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cDropdown id=\"cardLanguage\" onValueChanged=\"languageSelected(selectedIndex)\"\u003e\n \u003cOption\u003e简体中文\u003c/Option\u003e\n \u003cOption\u003e繁體中文\u003c/Option\u003e\n \u003cOption\u003eDeutsch\u003c/Option\u003e\n \u003cOption\u003eEnglish\u003c/Option\u003e\n \u003cOption\u003eEspañol\u003c/Option\u003e\n \u003cOption\u003eFrançais\u003c/Option\u003e\n \u003cOption\u003eItaliano\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e --\u003e\n\n \u003c!-- Option: splash scenario name on setup --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Fade in the name of the scenario for 2 seconds\u0026#xA;when placing down a scenario.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eShow scenario title on setup\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showTitleSplash\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Group: play area settings --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_compass\"\u003e\n \u003cText class=\"group-header\"\u003ePLAY AREA SETTINGS\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: play area snap tags --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Only cards with the tag 'Location' will snap (official cards are supported by default).\u0026#xA;Disable this if you are having issues with custom content.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eEnable snap tags\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"playAreaSnapTags\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: location connections --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Automatically draw location connections based on card metadata.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eDraw location connections\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"playAreaConnections\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: play area connection color --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"This color will be used to draw lines\u0026#xA;for location connections.\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eChoose color for location connections\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cButton id=\"playAreaConnectionColor\"\n onClick=\"onClick_playAreaConnectionColor\"\u003e\n \u003c/Button\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: change custom playarea image on setup --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Attempts to set the play area to a fitting image\u0026#xA;from the play area image gallery.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eChange background on setup\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"changePlayAreaImage\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Group: playermat settings --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_cover\"\u003e\n \u003cText class=\"group-header\"\u003ePLAYERMAT SETTINGS\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: enable snap tags --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Only cards with the tag 'Asset' will snap (official cards are supported by default).\u0026#xA;Disable this if you are having issues with custom content.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eEnable snap tags\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"useSnapTags\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show draw 1 button --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Displays a button below the 'Upkeep' button that draws a card from your deck.\u0026#xA;Useful for multi-handed solo play.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eShow \"Draw 1\" button\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showDrawButton\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: use clickable clue-counters --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Instead of automatically counting clues in the respective area on your playermat,\u0026#xA;this displays a clickable counter for clues.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eUse clickable clue counters\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"useClueClickers\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: use clickable resource counters --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"This enables spawning of clickable resource tokens for player cards.\u0026#xA;(Chef's Selection = assets with 0 uses)\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eUse clickable resource tokens\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cDropdown id=\"useResourceCounters\"\n onValueChanged=\"resourceCounterSelected(selectedIndex)\"\u003e\n \u003cOption\u003eEnabled\u003c/Option\u003e\n \u003cOption\u003eChef's Selection\u003c/Option\u003e\n \u003cOption\u003eDisabled\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: remove a player mat --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Remove an unused playermat for more table space.\u0026#xA;Displayed are the default colors.\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eRemove a playermat\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cDropdown id=\"removePlayermat\"\n onValueChanged=\"playermatRemovalSelected(selectedIndex)\"\u003e\n \u003cOption\u003eClick to select\u003c/Option\u003e\n \u003cOption\u003e1: White\u003c/Option\u003e\n \u003cOption\u003e2: Orange\u003c/Option\u003e\n \u003cOption\u003e3: Green\u003c/Option\u003e\n \u003cOption\u003e4: Red\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Group: fan-made accessories --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_olive\"\u003e\n \u003cText class=\"group-header\"\u003eFAN-MADE ACCESSORIES\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show attachment helper --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Provides a card-sized bag for cards that are attached to other cards\u0026#xA;(e.g. Backpack).\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eAttachment Helper\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showAttachmentHelper\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show clean up helper --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Useful for campaign-play:\u0026#xA;It resets play areas to allow continuous gameplay in the same savegame.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eClean Up Helper\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showCleanUpHelper\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show CYOA campaign guides --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Displays in a 'Choose Your Own Adventure'\u0026#xA;style redesigned campaign guides.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eCYOA Campaign Guides\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showCYOA\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show displacement tool --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"This allows moving all objects on the main play area\u0026#xA;in a chosen direction.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eDisplacement Tool\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showDisplacementTool\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show hand helper --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Never count your hand cards again! This tool does that for you\u0026#xA;and additionally enables easy discarding of random cards.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eHand Helper\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showHandHelper\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show search assistant --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Quickly search 3, 6, 9 or the top X\u0026#xA;cards of your deck!\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eSearch Assistant\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showSearchAssistant\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/VerticalScrollView\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Buttons: Defaults and Close --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cHorizontalLayout minHeight=\"55\"\n flexibleHeight=\"0\"\n padding=\"10 10 5 10\"\n spacing=\"225\"\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_defaultSettings\"\u003eLoad defaults\u003c/Button\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_toggleUi(optionPanel)\"\u003eClose\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include Global/OptionPanel.xml --\u003e\n\u003c!-- include Global/UpdateNotification.xml --\u003e\n\u003c!-- Default formatting inherented from OptionPanel! --\u003e\n\n\u003c!-- Icon with Finn, which can be clicked --\u003e\n\u003cImage id=\"FinnIcon\"\n active=\"false\"\n showAnimation=\"SlideIn_Top\"\n hideAnimation=\"SlideOut_Top\"\n animationDuration=\"0.2\"\n rectAlignment=\"UpperLeft\"\n offsetXY=\"420 -5\"\n height=\"90\"\n width=\"90\"\n onClick=\"onClick_toggleUi(updateNotification)\"\n image=\"FinnIcon\"\n tooltip=\"Update notification\"\n tooltipPosition=\"Right\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"/\u003e\n\n\u003c!-- main notification window --\u003e\n\u003cTableLayout id=\"updateNotification\"\n active=\"false\"\n color=\"#000000\"\n outlineSize=\"2 2\"\n outline=\"grey\"\n showAnimation=\"SlideIn_Top\"\n hideAnimation=\"SlideOut_Top\"\n animationDuration=\"0.2\"\n rectAlignment=\"UpperLeft\"\n offsetXY=\"60 -5\"\n height=\"225\"\n width=\"350\"\u003e\n\n \u003c!-- Header --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"10 10 0 0\"\u003e\n \u003c!-- this part will be updated via script --\u003e\n \u003cText id=\"notificationHeader\"\n font=\"font_teutonic-arkham\"\n fontSize=\"30\"\n alignment=\"MiddleCenter\"\u003ePlaceholder\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- patch highlights --\u003e\n \u003cRow id=\"highlightRow\"\n preferredHeight=\"100\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"15 15 0 7\"\u003e\n \u003c!-- this part will be updated via script --\u003e\n \u003cText id=\"releaseHighlightText\"\n resizeTextForBestFit=\"true\"\u003ePlaceholder\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- explanation --\u003e\n \u003cRow preferredHeight=\"25\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"15 15 0 7\"\u003e\n \u003cText resizeTextForBestFit=\"true\"\u003eVisit the usual place to receive this update.\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Buttons: \"Don't show again\" and \"Close\" --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cHorizontalLayout minHeight=\"55\"\n flexibleHeight=\"0\"\n padding=\"10 10 5 10\"\n spacing=\"10\"\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_notification(dontShowAgain)\"\u003eDon't show again\u003c/Button\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_notification(close)\"\u003eClose\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include Global/UpdateNotification.xml --\u003e\n\u003c!-- include Global/Global.xml --\u003e" + "XmlUI": "\u003c!-- include Global/Global.xml --\u003e\n\u003cDefaults\u003e\n \u003c!-- general stuff --\u003e\n \u003cText color=\"white\"\n fontSize=\"18\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- include Global/BottomBar.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton class=\"navbar\"\n tooltipPosition=\"Left\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\n color=\"clear\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- Buttons at the bottom right (height: n * 37 - 2) --\u003e\n\u003cVerticalLayout visibility=\"Admin\"\n color=\"#000000\"\n outlineSize=\"1 1\"\n outline=\"#303030\"\n rectAlignment=\"LowerRight\"\n width=\"35\"\n height=\"72\"\n offsetXY=\"-1 120\"\n spacing=\"2\"\u003e\n \u003cButton class=\"navbar\"\n icon=\"devourer\"\n tooltip=\"Downloadable Content\"\n onClick=\"onClick_toggleUi(downloadWindow)\"/\u003e\n \u003cButton class=\"navbar\"\n icon=\"option-gear\"\n tooltip=\"Options\"\n onClick=\"onClick_toggleUi(optionPanel)\"/\u003e\n\u003c/VerticalLayout\u003e\n\n\u003c!-- Navigation Overlay button (not visibly to Grey and Black) --\u003e\n\u003cPanel visibility=\"White|Brown|Red|Orange|Yellow|Green|Teal|Blue|Purple|Pink\"\n color=\"#000000\"\n outlineSize=\"1 1\"\n outline=\"#303030\"\n rectAlignment=\"LowerRight\"\n width=\"35\"\n height=\"35\"\n offsetXY=\"-1 85\"\u003e\n \u003cButton class=\"navbar\"\n icon=\"NavigationOverlayIcon\"\n tooltip=\"Navigation Overlay\"\n onClick=\"onClick_toggleUi(Navigation Overlay)\"/\u003e\n\u003c/Panel\u003e\n\u003c!-- include Global/BottomBar.xml --\u003e\n\u003c!-- include Global/DownloadWindow.xml --\u003e\n\u003cDefaults\u003e\n \u003cButton class=\"downloadTab\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n onClick=\"onClick_tab\"\n color=\"#888888\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"bGrey\"\n color=\"grey\"/\u003e\n \u003cButton class=\"bWhite\"\n color=\"white\"/\u003e\n \u003cButton class=\"activeTab\"\n color=\"#ffffff\"/\u003e\n \u003cButton class=\"windowButton\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n selectClass=\"bWhite\"\n color=\"#888888\"\n font=\"font_teutonic-arkham\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- window to select downloadable content --\u003e\n\u003cVerticalLayout id=\"downloadWindow\"\n color=\"black\"\n active=\"false\"\n height=\"800\"\n width=\"900\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\u003e\n\n \u003c!-- window header --\u003e\n \u003cPanel preferredHeight=\"60\"\n padding=\"10 10 5 5\"\n spacing=\"10\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\n color=\"black\"\u003e\n \u003cText fontSize=\"32\"\n font=\"font_teutonic-arkham\"\n preferredWidth=\"600\"\n alignment=\"MiddleLeft\"\u003eDownloadable Content\u003c/Text\u003e\n \u003cButton id=\"downloadAll_button\"\n class=\"windowButton\"\n visibility=\"Black\"\n onClick=\"onClick_downloadAll\"\n height=\"30\"\n preferredWidth=\"110\"\n fontSize=\"20\"\n tooltip=\"Very rough estimate: 400 MB\"\n tooltipPosition=\"Above\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\u003eDownload Everything\u003c/Button\u003e\n \u003cButton id=\"spawnPlaceholder_button\"\n class=\"windowButton\"\n visibility=\"Black\"\n onClick=\"onClick_spawnPlaceholder\"\n height=\"30\"\n preferredWidth=\"110\"\n fontSize=\"20\"\u003eSpawn Placeholder\u003c/Button\u003e\n \u003cPanel preferredWidth=\"50\"\u003e\n \u003cButton rectAlignment=\"MiddleRight\"\n width=\"50\"\n color=\"clear\"\n icon=\"close\"\n tooltip=\"Close\"\n tooltipPosition=\"Right\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\n onClick=\"onClick_toggleUi(downloadWindow)\"/\u003e\n \u003c/Panel\u003e\n \u003c/Panel\u003e\n\n \u003cHorizontalLayout\u003e\n \u003cVerticalLayout preferredWidth=\"600\"\u003e\n \u003c!-- tab selection --\u003e\n \u003cHorizontalLayout preferredHeight=\"60\"\n padding=\"5\"\n spacing=\"5\"\u003e\n \u003cButton class=\"downloadTab activeTab\"\n id=\"tab1\"\u003eOfficial Campaigns\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab2\"\u003eOfficial Scenarios\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab3\"\u003eFan-Made Campaigns\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab4\"\u003eFan-Made Scenarios\u003c/Button\u003e\n \u003cButton class=\"downloadTab\"\n id=\"tab5\"\u003eFan-Made Player Cards\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n\n \u003c!-- content list --\u003e\n \u003cVerticalScrollView color=\"transparent\"\n minHeight=\"100\"\n flexibleHeight=\"100\"\n scrollSensitivity=\"27\"\n scrollbarColors=\"grey|grey|#C8C8C8|rgba(0.78,0.78,0.78,0.5)\"\n horizontalScrollbarVisibility=\"AutohideAndExpandViewport\"\n raycastTarget=\"true\"\u003e\n \u003cVerticalLayout id=\"contentList\"\n padding=\"10 25 0 0\"\u003e\n \u003c!-- this will be filled via scripting --\u003e\n \u003c/VerticalLayout\u003e\n \u003c/VerticalScrollView\u003e\n \u003c/VerticalLayout\u003e\n\n \u003c!-- content preview window --\u003e\n \u003cVerticalLayout preferredWidth=\"300\"\n padding=\"15 15 15 5\"\u003e\n\n \u003c!-- header --\u003e\n \u003cVerticalLayout preferredHeight=\"110\"\u003e\n \u003cText id=\"previewTitle\"\n fontSize=\"28\"\n preferredHeight=\"70\"\n font=\"font_teutonic-arkham\"\u003ePreviewTitle\u003c/Text\u003e\n \u003cText id=\"previewAuthor\"\n fontSize=\"20\"\n preferredHeight=\"40\"\n font=\"font_teutonic-arkham\"\u003eby PreviewAuthor\u003c/Text\u003e\n \u003c/VerticalLayout\u003e\n\n \u003c!-- box art --\u003e\n \u003cPanel id=\"previewArtPanel\"\n preferredHeight=\"390\"\u003e\n \u003cMask id=\"previewArtMask\"\u003e\n \u003c!-- image will be set via scripting --\u003e\n \u003cImage id=\"previewArtImage\" /\u003e\n \u003c/Mask\u003e\n \u003c/Panel\u003e\n\n \u003c!-- description --\u003e\n \u003cPanel preferredHeight=\"160\"\u003e\n \u003cText id=\"previewDescription\"\n alignment=\"UpperLeft\"\n resizeTextForBestFit=\"true\"\n resizeTextMinSize=\"12\"\n resizeTextMaxSize=\"18\"\u003ePreviewDescription\u003c/Text\u003e\n \u003c/Panel\u003e\n\n \u003c!-- download button / progress bar (visibility handled by lua code)--\u003e\n \u003cPanel preferredHeight=\"60\"\u003e\n \u003c!-- download button --\u003e\n \u003cButton id=\"download_button\"\n class=\"windowButton\"\n onClick=\"onClick_download\"\n height=\"50\"\n width=\"270\"\n fontSize=\"28\"\u003eDownload\u003c/Button\u003e\n \u003c!-- download progress bar --\u003e\n \u003cProgressBar id=\"download_progress\"\n active=\"false\"\n height=\"50\"\n width=\"270\"\n percentage=\"0\"\n color=\"#111111\"\n textColor=\"#aaaaaa\"\n fillImageColor=\"#333333\"/\u003e\n \u003c/Panel\u003e\n \u003c/VerticalLayout\u003e\n \u003c/HorizontalLayout\u003e\n\u003c/VerticalLayout\u003e\n\u003c!-- include Global/DownloadWindow.xml --\u003e\n\u003c!-- include Global/PlayAreaGallery.xml --\u003e\n\u003cDefaults\u003e\n \u003c!-- type selection at the top --\u003e\n \u003cButton class=\"imageTab\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n onClick=\"b7b45b/onClick_imageTab\"\n color=\"#888888\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"bGrey\"\n color=\"grey\"/\u003e\n \u003cButton class=\"bWhite\"\n color=\"white\"/\u003e\n\n \u003cButton class=\"windowButton\"\n hoverClass=\"bGrey\"\n pressClass=\"bWhite\"\n selectClass=\"bWhite\"\n color=\"#888888\"\n font=\"font_teutonic-arkham\"/\u003e\n\n \u003c!-- image boxes in the grid --\u003e\n \u003cVerticalLayout class=\"imageBox\"\n color=\"black\"\n outline=\"#303030\"\n outlineSize=\"2 2\"\n onClick=\"b7b45b/onClick_image\"/\u003e\n \u003cImage class=\"playareaImage\"\n preferredHeight=\"330\"/\u003e\n \u003cText class=\"imageName\"\n preferredHeight=\"40\"\n resizeTextForBestFit=\"true\"\n resizeTextMinSize=\"10\"\n resizeTextMaxSize=\"18\"/\u003e\n\n \u003c!-- item selection on the left --\u003e\n \u003cText class=\"itemText\"\n alignment=\"MiddleLeft\"/\u003e\n \u003cPanel class=\"itemPanel\"\n preferredHeight=\"45\"\n onClick=\"b7b45b/onClick_listItem\"/\u003e\n\n \u003c!-- other --\u003e\n \u003cText class=\"headerText\"\n fontSize=\"35\"/\u003e\n \u003cVerticalLayout childForceExpandHeight=\"false\"/\u003e\n\u003c/Defaults\u003e\n\n\u003cVerticalLayout id=\"playAreaGallery\"\n active=\"false\"\n color=\"black\"\n height=\"880\"\n width=\"900\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\u003e\n\n \u003c!-- window header --\u003e\n \u003cPanel preferredHeight=\"60\"\n padding=\"10 10 5 5\"\n spacing=\"10\"\n outlineSize=\"2 2\"\n outline=\"#303030\"\n color=\"black\"\u003e\n \u003cText fontSize=\"32\"\n font=\"font_teutonic-arkham\"\n preferredWidth=\"600\"\n alignment=\"MiddleLeft\"\u003ePlayarea Image Gallery\u003c/Text\u003e\n \u003cButton id=\"customUrl_button\"\n class=\"windowButton\"\n onClick=\"onClick_customUrl\"\n height=\"30\"\n preferredWidth=\"125\"\n fontSize=\"24\"\u003eUse custom URL\u003c/Button\u003e\n \u003cPanel preferredWidth=\"50\"\u003e\n \u003cButton rectAlignment=\"MiddleRight\"\n width=\"50\"\n color=\"clear\"\n icon=\"close\"\n tooltip=\"Close\"\n tooltipPosition=\"Right\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"\n onClick=\"onClick_toggleUi(playAreaGallery)\"/\u003e\n \u003c/Panel\u003e\n \u003c/Panel\u003e\n\n \u003c!-- tab selection --\u003e\n \u003cHorizontalLayout preferredHeight=\"60\"\n padding=\"5\"\n spacing=\"5\"\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab1\"\u003eOfficial Campaigns\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab2\"\u003eOfficial Scenarios\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab3\"\u003eFan-Made Campaigns\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab4\"\u003eFan-Made Scenarios\u003c/Button\u003e\n \u003cButton class=\"imageTab\"\n id=\"imageTab5\"\u003eOther Images\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n\n \u003cHorizontalLayout preferredHeight=\"760\"\u003e\n \u003c!-- left column: navigation bar --\u003e\n \u003cVerticalLayout id=\"itemSelection\"\n preferredWidth=\"180\"\n padding=\"10 15 0 0\"\u003e\n \u003c!-- this will be filled via scripting --\u003e\n \u003c!-- \u003cPanel class=\"itemPanel\"\u003e\n \u003cText class=\"itemText\"\u003eItem\u003c/Text\u003e\n \u003c/Panel\u003e --\u003e\n \u003c/VerticalLayout\u003e\n\n \u003c!-- right column: image gallery --\u003e\n \u003cVerticalScrollView color=\"transparent\"\n minHeight=\"100\"\n flexibleHeight=\"100\"\n preferredWidth=\"720\"\n scrollSensitivity=\"380\"\n scrollbarColors=\"grey|grey|#C8C8C8|rgba(0.78,0.78,0.78,0.5)\"\n horizontalScrollbarVisibility=\"AutohideAndExpandViewport\"\n raycastTarget=\"true\"\u003e\n \u003cGridLayout id=\"playareaList\"\n preferredWidth=\"700\"\n padding=\"25 25 5 5\"\n spacing=\"10\"\n cellSize=\"330 370\"\u003e\n \u003c!-- this will be filled via scripting --\u003e\n \u003c!-- \u003cVerticalLayout class=\"imageBox\"\u003e\n \u003cImage class=\"playareaImage\" image=\"\"/\u003e\n \u003cText class=\"imageName\"\u003eImage Name\u003c/Text\u003e\n \u003c/VerticalLayout\u003e --\u003e\n \u003c/GridLayout\u003e\n \u003c/VerticalScrollView\u003e\n \u003c/HorizontalLayout\u003e\n\u003c/VerticalLayout\u003e\n\u003c!-- include Global/PlayAreaGallery.xml --\u003e\n\u003c!-- include Global/TitleSplash.xml --\u003e\n\u003c!-- Title Splash when starting a scenario --\u003e\n\u003cPanel id=\"title_splash\"\n height=\"220\"\n position=\"0 250 0\"\n showAnimation=\"FadeIn\"\n hideAnimation=\"FadeOut\"\n active=\"false\"\n animationDuration=\"2\"\u003e\n \u003cImage id=\"title_gradient\"\n height=\"220\"\n image=\"TitleGradient\" /\u003e\n \u003cText id=\"title_splash_text\"\n width=\"95%\"\n height=\"180\"\n resizeTextForBestFit=\"true\"\n resizeTextMinSize=\"100\"\n resizeTextMaxSize=\"150\"\n font=\"font_teutonic-arkham\"\n outline=\"black\"\n outlineSize=\"3 -3\"\n horizontalOverflow=\"Overflow\"\u003e\n \u003c/Text\u003e\n\u003c/Panel\u003e\n\u003c!-- include Global/TitleSplash.xml --\u003e\n\u003c!-- include Global/NavigationOverlay.xml --\u003e\n\u003c!-- Default formatting --\u003e\n\u003cDefaults\u003e\n \u003cText color=\"#FFFFFF\"\n alignment=\"MiddleLeft\" /\u003e\n\n \u003cToggle isOn=\"False\"\n rectAlignment=\"MiddleRight\" /\u003e\n\n \u003cCell dontUseTableCellBackground=\"true\"\n outlineSize=\"0 1\"\n outline=\"grey\" /\u003e\n\n \u003c!-- options --\u003e\n \u003cRow class=\"nav_option-text\"\n preferredHeight=\"45\"/\u003e\n \u003cCell class=\"nav_option-text\"\n color=\"#333333\"/\u003e\n \u003cCell class=\"nav_option-button\"\n color=\"#333333\"/\u003e\n \u003cText class=\"nav_option-header\"\n fontSize=\"20\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cCell class=\"claim\"\n tooltip=\"Clicking this seat in the navigation overlay\u0026#xA;will now only swap the playercolor for you.\"\n tooltipPosition=\"Right\" /\u003e\n\n \u003c!-- buttons at the bottom --\u003e\n \u003cButton class=\"bottomButtons\"\n hoverClass=\"hover\"\n pressClass=\"press\"\n selectClass=\"select\"\n color=\"#888888\"\n minHeight=\"35\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"hover\"\n color=\"grey\"/\u003e\n \u003cButton class=\"press\"\n color=\"white\"/\u003e\n \u003cButton class=\"select\"\n color=\"white\"/\u003e\n\n \u003c!-- Navigation Panels --\u003e\n \u003cPanel class=\"navPanel\"\n active=\"false\"\n allowDragging=\"true\"\n rectAlignment=\"LowerRight\"\n returnToOriginalPositionWhenReleased=\"false\"\n offsetXY=\"-40 0\"\u003e\n \u003c/Panel\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- full Panel --\u003e\n\u003cPanel id=\"navPanelFull\"\n height=\"358\"\n width=\"455\"\n class=\"navPanel\"\u003e\n\u003c/Panel\u003e\n\n\u003c!-- Play Area only --\u003e\n\u003cPanel id=\"navPanelPlay\"\n height=\"208\"\n width=\"205\"\n class=\"navPanel\"\u003e\n\u003c/Panel\u003e\n\n\u003c!-- Settings --\u003e\n\u003cTableLayout id=\"navPanelSettings\"\n active=\"false\"\n width=\"300\"\n height=\"380\"\n color=\"#000000\"\n outlineSize=\"2 2\"\n outline=\"grey\"\n rectAlignment=\"MiddleCenter\"\u003e\n\n \u003c!-- Header --\u003e\n \u003cRow preferredHeight=\"60\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"10 0 0 0\"\u003e\n \u003cText font=\"font_teutonic-arkham\"\n fontSize=\"35\"\u003eNavigation Overlay\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Options --\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cTableLayout columnWidths=\"0 125\"\n autoCalculateHeight=\"1\"\n cellPadding=\"10 0 5 5\"\u003e\n\n \u003c!-- Option: Custom pitch --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eViewing angle in degrees:\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button\"\u003e\n \u003cSlider id=\"sliderPitch\"\n onValueChanged=\"797ede/updatePitch\"\n wholeNumbers=\"true\"\n minValue=\"0\"\n maxValue=\"89\"\n value=\"75\"\n tooltip=\"This controls the camera pitch\u0026#xA;('nodding your head').\"\n tooltipPosition=\"Right\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Custom distance --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eViewing distance (relative):\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button\"\u003e\n \u003cSlider id=\"sliderDistance\"\n onValueChanged=\"797ede/updateDistance\"\n wholeNumbers=\"true\"\n minValue=\"50\"\n maxValue=\"200\"\n value=\"100\"\n tooltip=\"This controls the camera distance\u0026#xA;(from 50% to 200% of the default settings).\"\n tooltipPosition=\"Right\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim White --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"White\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimWhite\"\n onValueChanged=\"797ede/claimColor(White)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim Orange --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"Orange\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimOrange\"\n onValueChanged=\"797ede/claimColor(Orange)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim Green --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"Green\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimGreen\"\n onValueChanged=\"797ede/claimColor(Green)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Claim Red --\u003e\n \u003cRow class=\"nav_option-text\"\u003e\n \u003cCell class=\"nav_option-text\"\u003e\n \u003cText class=\"nav_option-header\"\u003eClaim \"Red\" seat\u003c/Text\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"nav_option-button claim\"\u003e\n \u003cToggle id=\"claimRed\"\n onValueChanged=\"797ede/claimColor(Red)\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Buttons: Defaults and Close --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cHorizontalLayout minHeight=\"55\"\n flexibleHeight=\"0\"\n padding=\"10 10 5 10\"\n spacing=\"35\"\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"797ede/loadDefaultSettings\"\u003eLoad defaults\u003c/Button\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"797ede/toggleSettings\"\u003eClose\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include Global/NavigationOverlay.xml --\u003e\n\u003c!-- include Global/OptionPanel.xml --\u003e\n\u003c!-- Default formatting --\u003e\n\u003cDefaults\u003e\n \u003cText color=\"#FFFFFF\"\n alignment=\"MiddleLeft\"/\u003e\n\n \u003cDropdown rectAlignment=\"MiddleCenter\"/\u003e\n\n \u003cCell dontUseTableCellBackground=\"true\"\n outlineSize=\"0 1\"\n outline=\"grey\"/\u003e\n\n \u003c!-- main window --\u003e\n \u003cTableLayout class=\"window\"\n width=\"500\"\n height=\"800\"\n active=\"false\"\n color=\"#000000\"\n outlineSize=\"2 2\"\n outline=\"grey\"\n showAnimation=\"SlideIn_Right\"\n hideAnimation=\"SlideOut_Right\"\n animationDuration=\"0.2\"/\u003e\n\n \u003c!-- group headers --\u003e\n \u003cRow class=\"group-header\"\n preferredHeight=\"44\"/\u003e\n \u003cCell class=\"group-header\"\n padding=\"10 10 0 0\"\n columnSpan=\"3\"\n color=\"#222222\"/\u003e\n \u003cPanel class=\"group-header\"\n padding=\"5 0 0 0\"/\u003e\n \u003cText class=\"group-header\"\n fontSize=\"28\"\n font=\"font_teutonic-arkham\"/\u003e\n\n \u003c!-- options --\u003e\n \u003cRow class=\"option-text\"\n preferredHeight=\"50\"\n tooltipPosition=\"Left\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"/\u003e\n \u003cCell class=\"option-text\"\n padding=\"10 10 5 5\"\n color=\"#333333\"\n columnSpan=\"2\"/\u003e\n \u003cCell class=\"option-button\"\n padding=\"10 10 5 5\"\n color=\"#333333\"/\u003e\n \u003cCell class=\"option-singleColumn\"\n padding=\"10 10 5 5\"\n color=\"#333333\"\n columnSpan=\"1\"/\u003e\n \u003cCell class=\"option-doubleColumn\"\n padding=\"10 10 5 5\"\n color=\"#333333\"\n columnSpan=\"2\"/\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\n padding=\"10 0 0 0\"/\u003e\n \u003cText class=\"option-header\"\n fontSize=\"22\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\n padding=\"0 17 3 3\"/\u003e\n \u003cButton class=\"optionToggle\"\n image=\"option_off\"\n rectAlignment=\"MiddleRight\"\n offsetXY=\"-30 0\"\n colors=\"#FFFFFF|#dfdfdf\"\n height=\"36\"\n width=\"65\"\n ignoreLayout=\"True\"/\u003e\n\n \u003c!-- buttons at the bottom --\u003e\n \u003cButton class=\"bottomButtons\"\n hoverClass=\"hover\"\n pressClass=\"press\"\n selectClass=\"select\"\n color=\"#888888\"\n minHeight=\"35\"\n fontSize=\"24\"\n font=\"font_teutonic-arkham\"/\u003e\n \u003cButton class=\"hover\"\n color=\"grey\"/\u003e\n \u003cButton class=\"press\"\n color=\"white\"/\u003e\n \u003cButton class=\"select\"\n color=\"white\"/\u003e\n\u003c/Defaults\u003e\n\n\u003c!-- Option Panel --\u003e\n\u003cTableLayout id=\"optionPanel\"\n class=\"window\"\n rectAlignment=\"LowerRight\"\n offsetXY=\"-50 80\"\n raycastTarget=\"true\"\u003e\n \u003c!-- Header: Options --\u003e\n \u003cRow preferredHeight=\"60\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"10 0 0 0\"\u003e\n \u003cText font=\"font_teutonic-arkham\"\n fontSize=\"35\"\u003eOptions\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Scrollable part with options --\u003e\n \u003cRow\u003e\n \u003cCell\u003e\n \u003cVerticalScrollView horizontalScrollbarVisibility=\"AutohideAndExpandViewport\"\n scrollSensitivity=\"30\"\n raycastTarget=\"true\"\u003e\n \u003cTableLayout columnWidths=\"0 100 75\"\n autoCalculateHeight=\"1\"\n useGlobalCellPadding=\"false\"\u003e\n\n \u003c!-- Group: general settings --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_acolyte\"\u003e\n \u003cText class=\"group-header\"\u003eGENERAL SETTINGS\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: card language --\u003e\n \u003c!-- disabled until we have the backend in place\n \u003cRow class=\"option-text\" tooltip=\"Downloading a campaign or importing a deck will use\u0026#xA;this language for cards (NOT FUNCTIONAL YET!).\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eCard language\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cDropdown id=\"cardLanguage\" onValueChanged=\"languageSelected(selectedIndex)\"\u003e\n \u003cOption\u003e简体中文\u003c/Option\u003e\n \u003cOption\u003e繁體中文\u003c/Option\u003e\n \u003cOption\u003eDeutsch\u003c/Option\u003e\n \u003cOption\u003eEnglish\u003c/Option\u003e\n \u003cOption\u003eEspañol\u003c/Option\u003e\n \u003cOption\u003eFrançais\u003c/Option\u003e\n \u003cOption\u003eItaliano\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e --\u003e\n\n \u003c!-- Option: splash scenario name on setup --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Fade in the name of the scenario for 2 seconds\u0026#xA;when placing down a scenario.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eShow scenario title on setup\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showTitleSplash\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: Enable all card helpers --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Enable all card helpers (usually enabled via context menu entries).\u0026#xA;Examples: False Covenant and Book of Living Myths\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eEnable all card helpers\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"enableCardHelpers\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Group: play area settings --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_compass\"\u003e\n \u003cText class=\"group-header\"\u003ePLAY AREA SETTINGS\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: play area snap tags --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Only cards with the tag 'Location' will snap (official cards are supported by default).\u0026#xA;Disable this if you are having issues with custom content.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eEnable snap tags\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"playAreaSnapTags\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: location connections --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Automatically draw location connections based on card metadata.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eDraw location connections\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"playAreaConnections\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: play area connection color --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"This color will be used to draw lines\u0026#xA;for location connections.\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eChoose color for location connections\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cButton id=\"playAreaConnectionColor\"\n onClick=\"onClick_playAreaConnectionColor\"\u003e\n \u003c/Button\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: change custom playarea image on setup --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Attempts to set the play area to a fitting image\u0026#xA;from the play area image gallery.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eChange background on setup\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"changePlayAreaImage\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Group: playermat settings --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_cover\"\u003e\n \u003cText class=\"group-header\"\u003ePLAYERMAT SETTINGS\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: enable snap tags --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Only cards with the tag 'Asset' will snap (official cards are supported by default).\u0026#xA;Disable this if you are having issues with custom content.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eEnable snap tags\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"useSnapTags\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show draw 1 button --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Displays a button below the 'Upkeep' button that draws a card from your deck.\u0026#xA;Useful for multi-handed solo play.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eShow \"Draw 1\" button\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showDrawButton\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: use class-specific texture --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Controls whether a class-specific playermat texture should be automatically loaded.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eUse class-specific texture\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"useClassTexture\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: use clickable clue-counters --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Instead of automatically counting clues in the respective area on your playermat,\u0026#xA;this displays a clickable counter for clues.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eUse clickable clue counters\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"useClueClickers\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: use clickable resource counters --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"This enables spawning of clickable resource tokens for player cards.\u0026#xA;(Chef's Selection = assets with 0 uses)\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eUse clickable resource tokens\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cDropdown id=\"useResourceCounters\"\n onValueChanged=\"resourceCounterSelected(selectedIndex)\"\u003e\n \u003cOption\u003eEnabled\u003c/Option\u003e\n \u003cOption\u003eChef's Selection\u003c/Option\u003e\n \u003cOption\u003eDisabled\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: remove a playermat --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Remove an unused playermat for more table space.\u0026#xA;Displayed are the default colors.\"\u003e\n \u003cCell class=\"option-singleColumn\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eRemove a playermat\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-doubleColumn\"\u003e\n \u003cPanel class=\"doubleColumn-wrapper\"\u003e\n \u003cDropdown id=\"removePlayermat\"\n onValueChanged=\"playermatRemovalSelected(selectedIndex)\"\u003e\n \u003cOption\u003eClick to select\u003c/Option\u003e\n \u003cOption\u003e1: White\u003c/Option\u003e\n \u003cOption\u003e2: Orange\u003c/Option\u003e\n \u003cOption\u003e3: Green\u003c/Option\u003e\n \u003cOption\u003e4: Red\u003c/Option\u003e\n \u003c/Dropdown\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Group: fan-made accessories --\u003e\n \u003cRow class=\"group-header\"\u003e\n \u003cCell class=\"group-header\"\u003e\n \u003cPanel class=\"group-header\"\n image=\"header_olive\"\u003e\n \u003cText class=\"group-header\"\u003eFAN-MADE ACCESSORIES\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show attachment helper --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Provides a card-sized bag for cards that are attached to other cards\u0026#xA;(e.g. Backpack).\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eAttachment Helper\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showAttachmentHelper\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show clean up helper --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Useful for campaign-play:\u0026#xA;It resets play areas to allow continuous gameplay in the same savegame.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eClean Up Helper\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showCleanUpHelper\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show CYOA campaign guides --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Displays in a 'Choose Your Own Adventure'\u0026#xA;style redesigned campaign guides.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eCYOA Campaign Guides\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showCYOA\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show displacement tool --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"This allows moving all objects on the main play area\u0026#xA;in a chosen direction.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eDisplacement Tool\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showDisplacementTool\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show hand helper --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Never count your hand cards again! This tool does that for you\u0026#xA;and additionally enables easy discarding of random cards.\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eHand Helper\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showHandHelper\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Option: show search assistant --\u003e\n \u003cRow class=\"option-text\"\n tooltip=\"Quickly search 3, 6, 9 or the top X\u0026#xA;cards of your deck!\"\u003e\n \u003cCell class=\"option-text\"\u003e\n \u003cPanel class=\"singleColumn-wrapper\"\u003e\n \u003cText class=\"option-header\"\u003eSearch Assistant\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003cCell class=\"option-button\"\u003e\n \u003cButton class=\"optionToggle\"\n id=\"showSearchAssistant\"\n onClick=\"onClick_toggleOption\"/\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n \u003c/TableLayout\u003e\n \u003c/VerticalScrollView\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Buttons: Defaults and Close --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cHorizontalLayout minHeight=\"55\"\n flexibleHeight=\"0\"\n padding=\"10 10 5 10\"\n spacing=\"225\"\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_defaultSettings\"\u003eLoad defaults\u003c/Button\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_toggleUi(optionPanel)\"\u003eClose\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include Global/OptionPanel.xml --\u003e\n\u003c!-- include Global/UpdateNotification.xml --\u003e\n\u003c!-- Default formatting inherented from OptionPanel! --\u003e\n\n\u003c!-- Icon with Finn, which can be clicked --\u003e\n\u003cImage id=\"FinnIcon\"\n active=\"false\"\n showAnimation=\"SlideIn_Top\"\n hideAnimation=\"SlideOut_Top\"\n animationDuration=\"0.2\"\n rectAlignment=\"UpperLeft\"\n offsetXY=\"420 -5\"\n height=\"90\"\n width=\"90\"\n onClick=\"onClick_toggleUi(updateNotification)\"\n image=\"FinnIcon\"\n tooltip=\"Update notification\"\n tooltipPosition=\"Right\"\n tooltipBackgroundColor=\"rgba(0,0,0,1)\"/\u003e\n\n\u003c!-- main notification window --\u003e\n\u003cTableLayout id=\"updateNotification\"\n active=\"false\"\n color=\"#000000\"\n outlineSize=\"2 2\"\n outline=\"grey\"\n showAnimation=\"SlideIn_Top\"\n hideAnimation=\"SlideOut_Top\"\n animationDuration=\"0.2\"\n rectAlignment=\"UpperLeft\"\n offsetXY=\"60 -5\"\n height=\"225\"\n width=\"350\"\u003e\n\n \u003c!-- Header --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"10 10 0 0\"\u003e\n \u003c!-- this part will be updated via script --\u003e\n \u003cText id=\"notificationHeader\"\n font=\"font_teutonic-arkham\"\n fontSize=\"30\"\n alignment=\"MiddleCenter\"\u003ePlaceholder\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- patch highlights --\u003e\n \u003cRow id=\"highlightRow\"\n preferredHeight=\"100\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"15 15 0 7\"\u003e\n \u003c!-- this part will be updated via script --\u003e\n \u003cText id=\"releaseHighlightText\"\n resizeTextForBestFit=\"true\"\u003ePlaceholder\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- explanation --\u003e\n \u003cRow preferredHeight=\"25\"\u003e\n \u003cCell\u003e\n \u003cPanel padding=\"15 15 0 7\"\u003e\n \u003cText resizeTextForBestFit=\"true\"\u003eVisit the usual place to receive this update.\u003c/Text\u003e\n \u003c/Panel\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\n \u003c!-- Buttons: \"Don't show again\" and \"Close\" --\u003e\n \u003cRow preferredHeight=\"50\"\u003e\n \u003cCell\u003e\n \u003cHorizontalLayout minHeight=\"55\"\n flexibleHeight=\"0\"\n padding=\"10 10 5 10\"\n spacing=\"10\"\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_notification(dontShowAgain)\"\u003eDon't show again\u003c/Button\u003e\n \u003cButton class=\"bottomButtons\"\n onClick=\"onClick_notification(close)\"\u003eClose\u003c/Button\u003e\n \u003c/HorizontalLayout\u003e\n \u003c/Cell\u003e\n \u003c/Row\u003e\n\u003c/TableLayout\u003e\n\u003c!-- include Global/UpdateNotification.xml --\u003e\n\u003c!-- include Global/Global.xml --\u003e" }